town tweak
This commit is contained in:
parent
84ad22f7af
commit
1ea8ba1701
12 changed files with 5005 additions and 379 deletions
469
mapTerrain.js
469
mapTerrain.js
|
|
@ -38,6 +38,15 @@ function quantile(values, q) {
|
|||
return lerp(arr[i], arr[Math.min(arr.length - 1, i + 1)], f);
|
||||
}
|
||||
|
||||
function softCapElevation(e, start = 0.91, cap = 1.08) {
|
||||
if (e <= start) return e;
|
||||
const over = e - start;
|
||||
const span = Math.max(0.001, cap - start);
|
||||
// Hard clipping made high mountains become flat mesas. This keeps peaks high,
|
||||
// but compresses only the excess so local relief survives near the top.
|
||||
return start + span * (1 - Math.exp(-over / span));
|
||||
}
|
||||
|
||||
function forDisk(cx, cy, radius, fn) {
|
||||
const r = Math.ceil(radius);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
|
|
@ -131,7 +140,12 @@ function ellipticalMask(px, py, system) {
|
|||
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 along = clamp((u / a + 1) * 0.5);
|
||||
const widthWave = 1
|
||||
+ (system.widthVariance ?? 0.28) * Math.sin((along + (system.phase ?? 0)) * Math.PI * 2.0)
|
||||
+ (system.widthVariance ?? 0.28) * 0.50 * Math.sin((along * 2.7 + (system.phase ?? 0) * 1.7) * Math.PI * 2.0);
|
||||
const endTaper = lerp(0.60, 1.0, Math.sin(along * Math.PI));
|
||||
const b = Math.max(0.012, system.width * 0.5 * clamp(widthWave, 0.62, 1.60) * endTaper);
|
||||
const r = Math.sqrt((u / a) ** 2 + (v / b) ** 2);
|
||||
return clamp(1 - smoothstep((r - 0.55) / 0.65));
|
||||
}
|
||||
|
|
@ -142,35 +156,203 @@ function sampleInsideUnitDisk(seed, n) {
|
|||
return { x: Math.cos(a) * r, y: Math.sin(a) * r, r };
|
||||
}
|
||||
|
||||
const TERRAIN_TYPES = [
|
||||
{
|
||||
id: "tohoku_spine",
|
||||
label: "東北型・長大脊梁",
|
||||
weight: 0.24,
|
||||
coastStyle: "parallel_spine",
|
||||
mountainMode: "range",
|
||||
massifnessRange: [0.06, 0.26],
|
||||
seaRatioRange: [0.13, 0.23],
|
||||
twoSidedChance: 0.96,
|
||||
mountainOffsetRange: [0.47, 0.53],
|
||||
baseHeightRange: [0.74, 1.10],
|
||||
primaryLengthRange: [0.76, 0.96],
|
||||
primaryWidthRange: [0.17, 0.30],
|
||||
systemCountRange: [12, 16],
|
||||
beltCountRange: [3, 4],
|
||||
angleSpread: 0.14,
|
||||
crossSpread: 0.54,
|
||||
lengthScale: 1.22,
|
||||
widthScale: 1.16,
|
||||
heightScale: 1.24,
|
||||
coastStrength: 0.90,
|
||||
plainBiasRange: [0.16, 0.34],
|
||||
riverRichnessRange: [0.70, 1.18],
|
||||
bigRiverChanceRange: [0.22, 0.46],
|
||||
},
|
||||
{
|
||||
id: "chubu_mountain",
|
||||
label: "中部型・交差高山地",
|
||||
weight: 0.24,
|
||||
coastStyle: "outer_coast",
|
||||
mountainMode: "massif",
|
||||
massifnessRange: [0.42, 0.74],
|
||||
seaRatioRange: [0.10, 0.20],
|
||||
twoSidedChance: 0.20,
|
||||
mountainOffsetRange: [0.16, 0.36],
|
||||
baseHeightRange: [0.68, 1.04],
|
||||
primaryLengthRange: [0.62, 0.92],
|
||||
primaryWidthRange: [0.30, 0.58],
|
||||
systemCountRange: [16, 20],
|
||||
beltCountRange: [3, 5],
|
||||
angleSpread: 0.92,
|
||||
crossSpread: 0.82,
|
||||
lengthScale: 1.34,
|
||||
widthScale: 1.30,
|
||||
heightScale: 1.12,
|
||||
coastStrength: 0.74,
|
||||
plainBiasRange: [0.08, 0.24],
|
||||
riverRichnessRange: [0.72, 1.12],
|
||||
bigRiverChanceRange: [0.28, 0.58],
|
||||
},
|
||||
{
|
||||
id: "setouchi_inland_sea",
|
||||
label: "瀬戸内型・内海多島",
|
||||
weight: 0.16,
|
||||
coastStyle: "inland_sea",
|
||||
mountainMode: "mixed",
|
||||
massifnessRange: [0.24, 0.48],
|
||||
seaRatioRange: [0.20, 0.33],
|
||||
twoSidedChance: 0.92,
|
||||
mountainOffsetRange: [0.22, 0.34],
|
||||
baseHeightRange: [0.56, 1.00],
|
||||
primaryLengthRange: [0.52, 0.76],
|
||||
primaryWidthRange: [0.18, 0.34],
|
||||
systemCountRange: [12, 16],
|
||||
beltCountRange: [2, 3],
|
||||
angleSpread: 0.24,
|
||||
crossSpread: 0.70,
|
||||
lengthScale: 0.98,
|
||||
widthScale: 1.08,
|
||||
heightScale: 1.08,
|
||||
coastStrength: 1.10,
|
||||
plainBiasRange: [0.26, 0.50],
|
||||
riverRichnessRange: [0.58, 0.96],
|
||||
bigRiverChanceRange: [0.18, 0.42],
|
||||
},
|
||||
{
|
||||
id: "kanto_alluvial",
|
||||
label: "関東・濃尾型・大河川平野",
|
||||
weight: 0.16,
|
||||
coastStyle: "open_bay",
|
||||
mountainMode: "range",
|
||||
massifnessRange: [0.18, 0.44],
|
||||
seaRatioRange: [0.15, 0.26],
|
||||
twoSidedChance: 0.18,
|
||||
mountainOffsetRange: [0.28, 0.46],
|
||||
baseHeightRange: [0.62, 1.04],
|
||||
primaryLengthRange: [0.42, 0.70],
|
||||
primaryWidthRange: [0.20, 0.36],
|
||||
systemCountRange: [10, 14],
|
||||
beltCountRange: [2, 3],
|
||||
angleSpread: 0.34,
|
||||
crossSpread: 0.62,
|
||||
lengthScale: 0.92,
|
||||
widthScale: 1.10,
|
||||
heightScale: 1.10,
|
||||
coastStrength: 0.92,
|
||||
plainBiasRange: [0.56, 0.86],
|
||||
riverRichnessRange: [0.98, 1.38],
|
||||
bigRiverChanceRange: [0.62, 0.90],
|
||||
},
|
||||
{
|
||||
id: "mixed_archipelago",
|
||||
label: "混合型・列島変化",
|
||||
weight: 0.20,
|
||||
coastStyle: "mixed_archipelago",
|
||||
mountainMode: "mixed",
|
||||
massifnessRange: [0.16, 0.72],
|
||||
seaRatioRange: [0.13, 0.29],
|
||||
twoSidedChance: 0.42,
|
||||
mountainOffsetRange: [0.18, 0.40],
|
||||
baseHeightRange: [0.68, 1.18],
|
||||
primaryLengthRange: [0.46, 0.82],
|
||||
primaryWidthRange: [0.20, 0.48],
|
||||
systemCountRange: [14, 17],
|
||||
beltCountRange: [3, 4],
|
||||
angleSpread: 0.50,
|
||||
crossSpread: 0.74,
|
||||
lengthScale: 1.00,
|
||||
widthScale: 1.18,
|
||||
heightScale: 1.25,
|
||||
coastStrength: 0.96,
|
||||
plainBiasRange: [0.22, 0.52],
|
||||
riverRichnessRange: [0.72, 1.26],
|
||||
bigRiverChanceRange: [0.34, 0.68],
|
||||
},
|
||||
];
|
||||
|
||||
function pickTerrainType(seed) {
|
||||
const total = TERRAIN_TYPES.reduce((sum, type) => sum + type.weight, 0);
|
||||
let r = rand(seed, 10001) * total;
|
||||
for (const type of TERRAIN_TYPES) {
|
||||
r -= type.weight;
|
||||
if (r <= 0) return type;
|
||||
}
|
||||
return TERRAIN_TYPES[TERRAIN_TYPES.length - 1];
|
||||
}
|
||||
|
||||
function rangeValue(seed, salt, [lo, hi]) {
|
||||
return lo + rand(seed, salt) * (hi - lo);
|
||||
}
|
||||
|
||||
function rangeInt(seed, salt, [lo, hi]) {
|
||||
return Math.round(lo + rand(seed, salt) * (hi - lo));
|
||||
}
|
||||
|
||||
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
|
||||
const terrainType = pickTerrainType(seed);
|
||||
const mountainMode = terrainType.mountainMode === "mixed"
|
||||
? (rand(seed, 12) < 0.42 ? "range" : rand(seed, 13) < 0.72 ? "mixed" : "massif")
|
||||
: terrainType.mountainMode;
|
||||
let mountainMassifness = rangeValue(seed, 14, terrainType.massifnessRange);
|
||||
if (mountainMode === "range") mountainMassifness *= 0.70;
|
||||
if (mountainMode === "massif") mountainMassifness = clamp(mountainMassifness + 0.12);
|
||||
|
||||
let coastAngle = rand(seed, 21) * Math.PI * 2;
|
||||
let twoSidedCoast = rand(seed, 22) < terrainType.twoSidedChance;
|
||||
const seaRatio = rangeValue(seed, 23, terrainType.seaRatioRange);
|
||||
let mountainAngle = coastAngle + Math.PI * (rangeValue(seed, 24, terrainType.mountainOffsetRange));
|
||||
if (terrainType.id === "tohoku_spine") {
|
||||
// 東北型は左右端または上下端に海を置き、海岸線にほぼ平行な長大脊梁を通す。
|
||||
// coastAngle は海へ向かう勾配方向、等値線としての海岸線は +90° 方向。
|
||||
coastAngle = (rand(seed, 2101) < 0.5 ? 0 : Math.PI / 2) + (rand(seed, 2102) - 0.5) * 0.10;
|
||||
twoSidedCoast = true;
|
||||
mountainAngle = coastAngle + Math.PI / 2 + (rand(seed, 2103) - 0.5) * 0.16;
|
||||
}
|
||||
const baseHeight = rangeValue(seed, 25, terrainType.baseHeightRange);
|
||||
const primaryLength = rangeValue(seed, 26, terrainType.primaryLengthRange);
|
||||
const primaryWidth = rangeValue(seed, 28, terrainType.primaryWidthRange);
|
||||
const scratchCount = Math.round(lerp(24 + rand(seed, 30) * 20, 16 + rand(seed, 31) * 18, mountainMassifness));
|
||||
const mountainSystemCount = rangeInt(seed, 32, terrainType.systemCountRange);
|
||||
const mountainBeltCount = rangeInt(seed, 46, terrainType.beltCountRange);
|
||||
|
||||
return {
|
||||
seed,
|
||||
terrainType: terrainType.id,
|
||||
terrainTypeLabel: terrainType.label,
|
||||
coastStyle: terrainType.coastStyle,
|
||||
seaRatio,
|
||||
coastAngle,
|
||||
twoSidedCoast,
|
||||
coastNoise: 0.045 + rand(seed, 33) * 0.045,
|
||||
coastNoise: 0.040 + rand(seed, 33) * 0.056,
|
||||
coastStrength: terrainType.coastStrength,
|
||||
mountainMode,
|
||||
mountainMassifness,
|
||||
mountainAngle,
|
||||
mountainAngleSpread: terrainType.angleSpread,
|
||||
mountainCrossSpread: terrainType.crossSpread,
|
||||
mountainLengthScale: terrainType.lengthScale,
|
||||
mountainWidthScale: terrainType.widthScale,
|
||||
mountainHeightScale: terrainType.heightScale,
|
||||
mountainBeltCount,
|
||||
mountainBaseHeight: baseHeight,
|
||||
mountainDensity: 0.62 + rand(seed, 34) * 0.35,
|
||||
mountainDensity: 0.58 + rand(seed, 34) * 0.39,
|
||||
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),
|
||||
x: clamp(0.50 + (rand(seed, 35) - 0.5) * 0.36, 0.18, 0.82),
|
||||
y: clamp(0.50 + (rand(seed, 36) - 0.5) * 0.36, 0.18, 0.82),
|
||||
angle: mountainAngle,
|
||||
length: primaryLength,
|
||||
width: primaryWidth,
|
||||
|
|
@ -186,9 +368,9 @@ export function buildTerrainTemplate(seed) {
|
|||
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,
|
||||
riverRichness: rangeValue(seed, 43, terrainType.riverRichnessRange),
|
||||
bigRiverChance: rangeValue(seed, 44, terrainType.bigRiverChanceRange),
|
||||
plainBias: rangeValue(seed, 45, terrainType.plainBiasRange),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -196,85 +378,146 @@ function buildMountainSystems(template, seed) {
|
|||
const systems = [];
|
||||
const targetCount = Math.max(8, template.mountainSystemCount ?? 15);
|
||||
const baseAngle = template.mountainAngle;
|
||||
const angleSpread = template.mountainAngleSpread ?? 0.42;
|
||||
const crossSpread = template.mountainCrossSpread ?? 0.70;
|
||||
const lengthScale = template.mountainLengthScale ?? 1;
|
||||
const widthScale = template.mountainWidthScale ?? 1;
|
||||
const heightScale = template.mountainHeightScale ?? 1;
|
||||
const isChubu = template.terrainType === "chubu_mountain";
|
||||
const isTohoku = template.terrainType === "tohoku_spine";
|
||||
|
||||
// 複数の脊梁山脈システムを、画面中央ではなくマップ全域に分散配置する。
|
||||
// 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);
|
||||
// 山脈システムは完全ランダムではなく、複数の広い造山帯に沿って配置する。
|
||||
// これにより「方向性はそこそこ揃う」が、「中央一点に集まらない」分布になる。
|
||||
const beltCount = Math.max(1, template.mountainBeltCount ?? (targetCount >= 15 ? 4 : 3));
|
||||
const belts = [];
|
||||
for (let b = 0; b < beltCount; b++) {
|
||||
const t = beltCount === 1 ? 0 : (b / (beltCount - 1) - 0.5);
|
||||
const angle = baseAngle + (rand(seed, 1000 + b) - 0.5) * angleSpread;
|
||||
const axisX = Math.cos(angle);
|
||||
const axisY = Math.sin(angle);
|
||||
const crossX = Math.cos(angle + Math.PI / 2);
|
||||
const crossY = Math.sin(angle + Math.PI / 2);
|
||||
const crossOffset = t * crossSpread + (rand(seed, 1010 + b) - 0.5) * (0.10 + crossSpread * 0.08);
|
||||
const alongShift = (rand(seed, 1020 + b) - 0.5) * 0.22;
|
||||
belts.push({
|
||||
angle,
|
||||
x: clamp(0.50 + axisX * alongShift / ASPECT + crossX * crossOffset / ASPECT, 0.08, 0.92),
|
||||
y: clamp(0.50 + axisY * alongShift + crossY * crossOffset, 0.08, 0.92),
|
||||
lengthBias: 0.82 + rand(seed, 1030 + b) * 0.32,
|
||||
heightBias: 0.82 + rand(seed, 1040 + b) * 0.42,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
function beltCandidate(k, attempt) {
|
||||
const beltIndex = (k + Math.floor(k / beltCount)) % beltCount;
|
||||
const belt = belts[beltIndex];
|
||||
const perBelt = Math.ceil(targetCount / beltCount);
|
||||
const ordinal = Math.floor(k / beltCount);
|
||||
const baseT = perBelt <= 1 ? 0 : ordinal / (perBelt - 1) - 0.5;
|
||||
const alongJitter = (rand(seed, 1100 + k * 79 + attempt * 11) - 0.5) * (attempt < 4 ? 0.15 : 0.28);
|
||||
const crossJitter = (rand(seed, 1200 + k * 79 + attempt * 11) - 0.5) * (attempt < 4 ? crossSpread * 0.22 : crossSpread * 0.40);
|
||||
const along = (baseT + alongJitter) * 1.03 * belt.lengthBias;
|
||||
const cross = crossJitter;
|
||||
const axisX = Math.cos(belt.angle);
|
||||
const axisY = Math.sin(belt.angle);
|
||||
const crossX = Math.cos(belt.angle + Math.PI / 2);
|
||||
const crossY = Math.sin(belt.angle + Math.PI / 2);
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
angle: baseAngle + localTurn + diagonalBias,
|
||||
x: clamp(belt.x + axisX * along / ASPECT + crossX * cross / ASPECT, 0.045, 0.955),
|
||||
y: clamp(belt.y + axisY * along + crossY * cross, 0.045, 0.955),
|
||||
angle: belt.angle + (rand(seed, 1300 + k * 79 + attempt) - 0.5) * angleSpread * 0.82,
|
||||
beltIndex,
|
||||
};
|
||||
}
|
||||
|
||||
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 edgeAwareScore(c) {
|
||||
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);
|
||||
return Math.min(edgeD, 0.16) * 0.16 + centerD * 0.08;
|
||||
}
|
||||
|
||||
function candidateAt(k, attempt) {
|
||||
return attempt < 5 ? gridCandidate(k, attempt) : randomCandidate(k, attempt);
|
||||
if (isTohoku) {
|
||||
const centralAngle = baseAngle + (rand(seed, 3330) - 0.5) * 0.06;
|
||||
const centralAlong = (rand(seed, 3331) - 0.5) * 0.10;
|
||||
const centralCross = (rand(seed, 3332) - 0.5) * 0.045;
|
||||
systems.push({
|
||||
x: clamp(0.50 + Math.cos(centralAngle) * centralAlong / ASPECT + Math.cos(centralAngle + Math.PI / 2) * centralCross / ASPECT, 0.12, 0.88),
|
||||
y: clamp(0.50 + Math.sin(centralAngle) * centralAlong + Math.sin(centralAngle + Math.PI / 2) * centralCross, 0.12, 0.88),
|
||||
angle: centralAngle,
|
||||
length: (0.78 + rand(seed, 3333) * 0.18) * lengthScale,
|
||||
width: (0.17 + rand(seed, 3334) * 0.11) * widthScale,
|
||||
height: template.mountainBaseHeight * heightScale * (0.58 + rand(seed, 3335) * 0.18),
|
||||
scratchCount: Math.round(20 + rand(seed, 3336) * 10),
|
||||
massifness: clamp((template.mountainMassifness ?? 0.16) * 0.55),
|
||||
role: "central-primary",
|
||||
beltIndex: 0,
|
||||
widthVariance: 0.30 + rand(seed, 3337) * 0.46,
|
||||
phase: rand(seed, 3338),
|
||||
});
|
||||
|
||||
if (rand(seed, 3339) < 0.72) {
|
||||
const side = rand(seed, 3340) < 0.5 ? -1 : 1;
|
||||
systems.push({
|
||||
x: clamp(0.50 + Math.cos(centralAngle) * (centralAlong + side * 0.18) / ASPECT + Math.cos(centralAngle + Math.PI / 2) * (centralCross + side * 0.028) / ASPECT, 0.10, 0.90),
|
||||
y: clamp(0.50 + Math.sin(centralAngle) * (centralAlong + side * 0.18) + Math.sin(centralAngle + Math.PI / 2) * (centralCross + side * 0.028), 0.10, 0.90),
|
||||
angle: centralAngle + (rand(seed, 3341) - 0.5) * 0.08,
|
||||
length: (0.48 + rand(seed, 3342) * 0.20) * lengthScale,
|
||||
width: (0.11 + rand(seed, 3343) * 0.08) * widthScale,
|
||||
height: template.mountainBaseHeight * heightScale * (0.38 + rand(seed, 3344) * 0.16),
|
||||
scratchCount: Math.round(12 + rand(seed, 3345) * 8),
|
||||
massifness: clamp((template.mountainMassifness ?? 0.16) * 0.65),
|
||||
role: "central-secondary",
|
||||
beltIndex: 0,
|
||||
widthVariance: 0.24 + rand(seed, 3346) * 0.36,
|
||||
phase: rand(seed, 3347),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let k = 0; k < targetCount; k++) {
|
||||
let best = candidateAt(k, 0);
|
||||
for (let k = systems.length; k < targetCount; k++) {
|
||||
let best = beltCandidate(k, 0);
|
||||
let bestScore = -INF;
|
||||
for (let attempt = 0; attempt < 18; attempt++) {
|
||||
const c = candidateAt(k, attempt);
|
||||
for (let attempt = 0; attempt < 12; attempt++) {
|
||||
const c = beltCandidate(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;
|
||||
const score = minD * 1.05 + edgeAwareScore(c) + rand(seed, 2300 + k * 101 + attempt) * 0.035;
|
||||
if (score > bestScore) { bestScore = score; best = c; }
|
||||
}
|
||||
|
||||
const m = clamp(template.mountainMassifness + (rand(seed, 2400 + k) - 0.5) * 0.50);
|
||||
const belt = belts[best.beltIndex];
|
||||
const m = clamp(template.mountainMassifness + (rand(seed, 2400 + k) - 0.5) * 0.38);
|
||||
const isMassif = m > 0.58;
|
||||
const major = k < 4 || rand(seed, 2500 + k) > 0.68;
|
||||
const major = k < beltCount || rand(seed, 2500 + k) > 0.72;
|
||||
const lengthBaseRange = isChubu
|
||||
? (major ? [0.48, 0.78] : [0.34, 0.58])
|
||||
: isTohoku
|
||||
? (major ? [0.44, 0.72] : [0.28, 0.48])
|
||||
: (major ? [0.32, 0.52] : [0.21, 0.36]);
|
||||
const lengthMassifRange = isChubu
|
||||
? (major ? [0.38, 0.58] : [0.28, 0.44])
|
||||
: (major ? [0.23, 0.35] : [0.17, 0.27]);
|
||||
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,
|
||||
lengthBaseRange[0] + rand(seed, 2600 + k) * (lengthBaseRange[1] - lengthBaseRange[0]),
|
||||
lengthMassifRange[0] + rand(seed, 2620 + k) * (lengthMassifRange[1] - lengthMassifRange[0]),
|
||||
m
|
||||
);
|
||||
) * belt.lengthBias * lengthScale;
|
||||
const widthRangeA = major ? [0.082, 0.170] : [0.060, 0.120];
|
||||
const widthRangeB = major ? [0.150, 0.260] : [0.110, 0.200];
|
||||
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,
|
||||
widthRangeA[0] + rand(seed, 2700 + k) * (widthRangeA[1] - widthRangeA[0]),
|
||||
widthRangeB[0] + rand(seed, 2720 + k) * (widthRangeB[1] - widthRangeB[0]),
|
||||
m
|
||||
);
|
||||
const height = template.mountainBaseHeight * (
|
||||
) * widthScale * (0.82 + rand(seed, 2740 + k) * 0.46);
|
||||
const heightBase = template.mountainBaseHeight * belt.heightBias * heightScale * (
|
||||
major
|
||||
? 0.34 + rand(seed, 2800 + k) * 0.24
|
||||
: 0.20 + rand(seed, 2810 + k) * 0.18
|
||||
? 0.44 + rand(seed, 2800 + k) * 0.28
|
||||
: 0.26 + rand(seed, 2810 + k) * 0.20
|
||||
);
|
||||
const height = isChubu ? heightBase * 0.82 : heightBase;
|
||||
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,
|
||||
major ? 9 + rand(seed, 2900 + k) * 9 : 6 + rand(seed, 2910 + k) * 6,
|
||||
isMassif ? 8 + rand(seed, 2920 + k) * 8 : 6 + rand(seed, 2930 + k) * 6,
|
||||
m
|
||||
));
|
||||
|
||||
|
|
@ -287,7 +530,10 @@ function buildMountainSystems(template, seed) {
|
|||
height,
|
||||
scratchCount,
|
||||
massifness: m,
|
||||
role: major ? (k < 4 ? "primary" : "major") : "minor",
|
||||
role: major ? (k < beltCount ? "primary" : "major") : "minor",
|
||||
beltIndex: best.beltIndex,
|
||||
widthVariance: 0.18 + rand(seed, 3100 + k) * 0.46,
|
||||
phase: rand(seed, 3200 + k),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -308,8 +554,8 @@ function buildScratchRidges(system, seed, systemId) {
|
|||
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);
|
||||
const width = lerp(0.014 + rand(seed, 2300 + i) * 0.018, 0.024 + rand(seed, 2300 + i) * 0.026, system.massifness) * (0.85 + density * 0.70);
|
||||
const height = system.height * (0.060 + density * 0.128 + rand(seed, 2400 + i) * 0.050);
|
||||
ridges.push({
|
||||
x, y,
|
||||
angle: localAngle,
|
||||
|
|
@ -327,13 +573,45 @@ function buildScratchRidges(system, seed, systemId) {
|
|||
}
|
||||
|
||||
function computeCoastLower(px, py, template, seed) {
|
||||
const axis = (px - 0.5) * Math.cos(template.coastAngle) * ASPECT + (py - 0.5) * Math.sin(template.coastAngle);
|
||||
const angle = template.coastAngle;
|
||||
const axis = (px - 0.5) * Math.cos(angle) * ASPECT + (py - 0.5) * Math.sin(angle);
|
||||
const cross = -(px - 0.5) * Math.sin(angle) * ASPECT + (py - 0.5) * Math.cos(angle);
|
||||
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 };
|
||||
const islandNoise = (fbm(px * 420 + 17, py * 420 - 11, seed + 302) - 0.5) * 0.032;
|
||||
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;
|
||||
pressure = Math.max(sideA, sideB, channel);
|
||||
} else if (template.coastStyle === "parallel_spine") {
|
||||
// 東北型: 左右端または上下端に海を置く。海岸線は脊梁山脈とおおよそ平行。
|
||||
// 内海的な中央水路は作らない。
|
||||
const edgeA = smoothstep((-axis - 0.26 + wave * 0.42 + bay * 0.35) / 0.20);
|
||||
const edgeB = template.twoSidedCoast ? smoothstep((axis - 0.26 - wave * 0.42 + bay * 0.25) / 0.22) * 0.86 : 0;
|
||||
pressure = Math.max(edgeA, edgeB);
|
||||
} else if (template.coastStyle === "outer_coast") {
|
||||
// 中部型: 外縁海を中心にし、内陸へ海が入り込みすぎないようにする。
|
||||
const radial = distNorm(px, py, 0.5, 0.5);
|
||||
const outer = smoothstep((radial - 0.44 + wave * 0.8 + bay * 0.5) / 0.24);
|
||||
const side = smoothstep((-axis + 0.30 + wave) / 0.30) * 0.45;
|
||||
pressure = Math.max(outer, side);
|
||||
} else if (template.coastStyle === "open_bay") {
|
||||
// 関東・濃尾型: 一方向に開いた湾と、その背後の沖積平野を作りやすくする。
|
||||
const openSide = smoothstep((-axis + 0.29 + wave + bay) / 0.25);
|
||||
const bayMouth = smoothstep((0.22 - Math.abs(cross + wave * 0.8)) / 0.25) * smoothstep((-axis + 0.16 + bay) / 0.22) * 0.68;
|
||||
pressure = Math.max(openSide, bayMouth);
|
||||
} else {
|
||||
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 outerBite = smoothstep((distNorm(px, py, 0.5, 0.5) - 0.54 + islandNoise) / 0.22) * 0.25;
|
||||
pressure = Math.max(sideA, sideB, outerBite);
|
||||
}
|
||||
|
||||
return { pressure: clamp(pressure), signedAxis: axis };
|
||||
}
|
||||
|
||||
function recomputeSlope(elevation, sea, slope) {
|
||||
|
|
@ -593,10 +871,10 @@ function deriveFields(seed, template, fields, seaLevel) {
|
|||
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 low = clamp((0.52 - e) * 1.70);
|
||||
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));
|
||||
plain[i] = clamp(low * lowSlope * (0.12 + template.plainBias * 0.34 + riverGate * 0.84));
|
||||
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));
|
||||
|
|
@ -675,13 +953,13 @@ export function generateTerrainAndRivers(seed) {
|
|||
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 e = 0.42 + terrainLarge + terrainRegional - coastPressure * (terrainTemplate.coastStrength ?? 0.96) * (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);
|
||||
const broad = Math.pow(mask, lerp(1.70, 1.16, system.massifness)) * system.height * lerp(0.22, 0.34, system.massifness);
|
||||
e += broad;
|
||||
arcSpineField[i] = Math.max(arcSpineField[i], mask * (system.role === "minor" ? 0.42 : system.role === "primary" ? 0.86 : 0.72));
|
||||
}
|
||||
|
|
@ -698,7 +976,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
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);
|
||||
elevation[i] = clamp(softCapElevation(e, terrainTemplate.terrainType === "chubu_mountain" ? 0.88 : 0.91, 1.08), 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);
|
||||
|
|
@ -707,7 +985,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
|
||||
let seaLevel = quantile(elevation, terrainTemplate.seaRatio);
|
||||
seaLevel = clamp(seaLevel, 0.20, 0.47);
|
||||
seaLevel = clamp(seaLevel, 0.14, 0.47);
|
||||
classifyWater(elevation, seaLevel, sea, ocean, lake);
|
||||
recomputeSlope(elevation, sea, slope);
|
||||
|
||||
|
|
@ -721,8 +999,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
const prefectureMask = makePrefectureMask(seed, sea, elevation, slope, river);
|
||||
const regional = generateRegionalPrefectures(seed, sea, elevation, slope, river, ridgeField, flowAccum, prefectureMask);
|
||||
const prefectureRegionId = regional.regionId;
|
||||
const adminPrefectureRegionId = regional.displayRegionId || regional.regionId;
|
||||
const regionalDebug = regional.debug;
|
||||
const regionalPrefectureBorders = extractRegionBorderSegments(prefectureRegionId, sea);
|
||||
const regionalPrefectureBorders = extractRegionBorderSegments(adminPrefectureRegionId, sea);
|
||||
const prefectureBorder = extractMaskBorder(prefectureMask, sea);
|
||||
|
||||
let landCount = 0;
|
||||
|
|
@ -734,12 +1013,15 @@ export function generateTerrainAndRivers(seed) {
|
|||
for (let i = 0; i < SIZE; i++) {
|
||||
if (sea[i]) { waterCount++; continue; }
|
||||
landCount++;
|
||||
if (elevation[i] > 0.60 || ridgeField[i] > 0.58) mountainCount++;
|
||||
if (elevation[i] > 0.56 || ridgeField[i] > 0.52) mountainCount++;
|
||||
if (plain[i] > 0.36) plainCount++;
|
||||
if (arcSpineField[i] > 0.55) { primarySpineStrength += arcSpineField[i]; spineSamples++; }
|
||||
}
|
||||
primarySpineStrength /= Math.max(1, spineSamples);
|
||||
const terrainDebug = {
|
||||
terrainType: terrainTemplate.terrainType,
|
||||
terrainTypeLabel: terrainTemplate.terrainTypeLabel,
|
||||
coastStyle: terrainTemplate.coastStyle,
|
||||
primarySpineStrength,
|
||||
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,
|
||||
|
|
@ -790,6 +1072,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
prefectureMask,
|
||||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
adminPrefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue