tweak
This commit is contained in:
parent
6c10d5d70e
commit
3fe9c31453
12 changed files with 963 additions and 127 deletions
483
mapTerrain.js
483
mapTerrain.js
|
|
@ -8,6 +8,145 @@ 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 = rand(seed, 45) > 0.64 ? 2 : 1;
|
||||
const sideAPlain = 0.035 + rand(seed, 56) * 0.115 + deposition * 0.085;
|
||||
const sideBPlain = 0.035 + rand(seed, 57) * 0.115 + deposition * 0.085;
|
||||
|
||||
return {
|
||||
seed,
|
||||
spineCount,
|
||||
spineAngle: coastAngle + Math.PI * (0.28 + rand(seed, 46) * 0.44),
|
||||
spineCurve: (rand(seed, 47) - 0.5) * 0.28,
|
||||
spinePosition: (rand(seed, 48) - 0.5) * 0.56,
|
||||
spineStrength: 0.66 + rand(seed, 49) * 0.44,
|
||||
spineWidth: 0.060 + rand(seed, 50) * 0.050,
|
||||
secondaryMountainCount: 3 + Math.floor(rand(seed, 51) * 5),
|
||||
secondaryMountainSize: 0.060 + rand(seed, 52) * 0.085,
|
||||
secondaryMountainStrength: 0.55 + rand(seed, 53) * 0.55,
|
||||
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.24 + rand(seed, 58) * 0.24,
|
||||
inletStrength: 0.18 + rand(seed, 59) * 0.56,
|
||||
plainWidth: sideAPlain,
|
||||
},
|
||||
{
|
||||
penetration: 0.24 + rand(seed, 60) * 0.24,
|
||||
inletStrength: 0.18 + rand(seed, 61) * 0.56,
|
||||
plainWidth: sideBPlain,
|
||||
},
|
||||
],
|
||||
deposition,
|
||||
erosion,
|
||||
roughness,
|
||||
ridgeJaggedness,
|
||||
ridgeBranchiness: 0.28 + rand(seed, 55) * 0.62,
|
||||
};
|
||||
}
|
||||
|
||||
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 spineFieldAt(x, y, template, spineIndex) {
|
||||
const seed = template.seed || 0;
|
||||
const spacing = spineIndex === 0 ? 0 : (spineIndex % 2 ? 0.18 : -0.18);
|
||||
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.18 + rand(seed, 730 + spineIndex) * 0.08),
|
||||
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 buildSpineRidges(seed, template) {
|
||||
const spines = [];
|
||||
const branches = [];
|
||||
for (let i = 0; i < template.spineCount; i++) {
|
||||
const angle = template.spineAngle + (i - 0.5) * 0.17 + (rand(seed, 700 + i) - 0.5) * 0.18;
|
||||
const spacing = i === 0 ? 0 : (i % 2 ? 0.18 : -0.18);
|
||||
const x = 0.5 + Math.cos(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45;
|
||||
const y = 0.5 + Math.sin(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45;
|
||||
spines.push({
|
||||
x, y, angle,
|
||||
width: template.spineWidth * (0.82 + rand(seed, 710 + i) * 0.38),
|
||||
length: 0.78 + rand(seed, 720 + i) * 0.28,
|
||||
h: template.spineStrength * (0.18 + rand(seed, 730 + i) * 0.08),
|
||||
curve: template.spineCurve,
|
||||
axisWobble: template.spineWidth * (0.45 + template.ridgeJaggedness * 1.15),
|
||||
kinkFrequency: 10 + rand(seed, 740 + i) * 18,
|
||||
kinkPhase: rand(seed, 750 + i) * Math.PI * 2,
|
||||
seedOffset: 7600 + i * 211,
|
||||
widthVariation: 0.18 + template.ridgeJaggedness * 0.34,
|
||||
});
|
||||
const branchCount = 3 + Math.floor(template.ridgeBranchiness * 5);
|
||||
for (let b = 0; b < branchCount; b++) {
|
||||
const along = (rand(seed, 810 + i * 31 + b) - 0.5) * 0.62;
|
||||
const side = rand(seed, 820 + i * 31 + b) > 0.5 ? 1 : -1;
|
||||
const branchAngle = angle + side * (0.55 + rand(seed, 830 + i * 31 + b) * 0.72);
|
||||
branches.push({
|
||||
x: x + Math.cos(angle) * along,
|
||||
y: y + Math.sin(angle) * along,
|
||||
angle: branchAngle,
|
||||
width: template.spineWidth * (0.42 + rand(seed, 840 + i * 31 + b) * 0.36),
|
||||
length: 0.16 + rand(seed, 850 + i * 31 + b) * 0.28,
|
||||
h: template.spineStrength * (0.055 + template.ridgeBranchiness * 0.085 + rand(seed, 860 + i * 31 + b) * 0.055),
|
||||
curve: template.spineCurve * 0.45,
|
||||
axisWobble: template.spineWidth * (0.32 + template.ridgeJaggedness * 0.72),
|
||||
kinkFrequency: 14 + rand(seed, 870 + i * 31 + b) * 20,
|
||||
kinkPhase: rand(seed, 880 + i * 31 + b) * Math.PI * 2,
|
||||
seedOffset: 8800 + i * 311 + b * 37,
|
||||
widthVariation: 0.22 + template.ridgeJaggedness * 0.30,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { spines, branches };
|
||||
}
|
||||
|
||||
export function generateTerrainAndRivers(seed) {
|
||||
let prefectureMask;
|
||||
let prefectureBorder;
|
||||
|
|
@ -17,6 +156,8 @@ export function generateTerrainAndRivers(seed) {
|
|||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
ocean,
|
||||
lake,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
|
|
@ -28,35 +169,70 @@ export function generateTerrainAndRivers(seed) {
|
|||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
arcSpineField,
|
||||
branchRidgeField,
|
||||
depositionalLowland,
|
||||
alluvialFanField,
|
||||
deltaField,
|
||||
naturalBarrierScore,
|
||||
flowTo,
|
||||
portSuitability,
|
||||
crossingSuitability,
|
||||
passSuitability,
|
||||
} = createMapFields();
|
||||
|
||||
const coastAngle = rand(seed, 11) * Math.PI * 2;
|
||||
const terrainTemplate = buildTerrainTemplate(seed);
|
||||
const coastAngle = terrainTemplate.coastAngle;
|
||||
const coastX = Math.cos(coastAngle);
|
||||
const coastY = Math.sin(coastAngle);
|
||||
const coastThreshold = 0.22 + rand(seed, 12) * 0.22;
|
||||
const coastStrength = 0.15 + rand(seed, 13) * 0.23;
|
||||
const coastThreshold = terrainTemplate.coastBias;
|
||||
const coastStrength = 0.13 + (1 - terrainTemplate.deposition) * 0.16 + rand(seed, 13) * 0.13;
|
||||
const { spines, branches } = buildSpineRidges(seed, terrainTemplate);
|
||||
|
||||
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.285;
|
||||
|
||||
const mountainBlobs = Array.from({ length: 2 + Math.floor(rand(seed, 98) * 3) }, (_, i) => ({
|
||||
x: rand(seed, 100 + i) * MAP_W,
|
||||
y: rand(seed, 200 + i) * MAP_H,
|
||||
r: 10 + rand(seed, 300 + i) * 24,
|
||||
h: 0.08 + rand(seed, 400 + i) * 0.16,
|
||||
}));
|
||||
|
||||
const ridgeBands = Array.from({ length: 5 + Math.floor(rand(seed, 97) * 4) }, (_, i) => ({
|
||||
x: rand(seed, 1500 + i) * MAP_W,
|
||||
y: rand(seed, 1600 + i) * MAP_H,
|
||||
angle: rand(seed, 1700 + i) * Math.PI * 2,
|
||||
width: 3 + rand(seed, 1800 + i) * 7,
|
||||
length: 42 + rand(seed, 1900 + i) * 92,
|
||||
h: 0.11 + rand(seed, 2000 + i) * 0.22,
|
||||
}));
|
||||
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);
|
||||
}
|
||||
return {
|
||||
x: clamp(x) * MAP_W,
|
||||
y: clamp(y) * MAP_H,
|
||||
r: (terrainTemplate.secondaryMountainSize * (0.72 + rand(seed, 300 + i) * 0.72)) * Math.min(MAP_W, MAP_H),
|
||||
h: terrainTemplate.secondaryMountainStrength * (0.08 + rand(seed, 400 + i) * 0.17),
|
||||
};
|
||||
});
|
||||
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
|
|
@ -75,26 +251,22 @@ export function generateTerrainAndRivers(seed) {
|
|||
mountains += Math.exp(-d * d * 2.35) * blob.h;
|
||||
}
|
||||
|
||||
let ridges = 0;
|
||||
for (const ridge of ridgeBands) {
|
||||
const dx = wx - ridge.x;
|
||||
const dy = wy - ridge.y;
|
||||
const along = dx * Math.cos(ridge.angle) + dy * Math.sin(ridge.angle);
|
||||
const perp = -dx * Math.sin(ridge.angle) + dy * Math.cos(ridge.angle);
|
||||
const lengthFade = smoothstep(1 - Math.abs(along) / ridge.length);
|
||||
const serration = 0.72 + valueNoise(wx + along * 0.15, wy + perp * 0.15, seed + 2220, 8) * 0.56;
|
||||
ridges += Math.exp(-(perp * perp) / (ridge.width * ridge.width)) * lengthFade * ridge.h * serration;
|
||||
}
|
||||
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);
|
||||
const ridges = spineRidges + branchRidges;
|
||||
|
||||
const directionalCoast = nx * coastX + ny * coastY;
|
||||
const coastWave = (fbm(wx * 0.72, wy * 0.72, seed + 2222) - 0.5) * 0.12 + (valueNoise(wx, wy, seed + 2233, 18) - 0.5) * 0.08;
|
||||
const coastLower = smoothstep((directionalCoast + coastWave - coastThreshold) / 0.26);
|
||||
const coast = coastPressureAt(x, y, wx, wy);
|
||||
const coastLower = coast.pressure;
|
||||
// Four terrain-noise bands from continental structure to fine surface roughness.
|
||||
const terrainLarge = fbm(wx * 0.36 + 40, wy * 0.36 - 60, seed + 710);
|
||||
const terrainRegional = fbm(wx * 0.95 + 80, wy * 0.95 - 20, seed + 777);
|
||||
const terrainLocal = fbm(wx * 2.05 + 17, wy * 2.05 - 31, seed + 1777);
|
||||
const terrainFine = valueNoise(wx * 2.9 + 11, wy * 2.9 - 19, seed + 2444, 4.5);
|
||||
const fineDissection = Math.abs(terrainLocal - 0.5) * 0.08 + Math.abs(terrainFine - 0.5) * 0.035;
|
||||
const fineDissection = (Math.abs(terrainLocal - 0.5) * 0.08 + Math.abs(terrainFine - 0.5) * 0.035) * (0.68 + terrainTemplate.roughness * 0.74);
|
||||
const basin = 0.1 * Math.sin((nx * 3.1 + ny * 1.7 + rand(seed, 15)) * Math.PI) - 0.045 * Math.cos((nx * 5.2 - ny * 3.6 + rand(seed, 16)) * Math.PI);
|
||||
const rawElevation =
|
||||
0.30 * terrainLarge +
|
||||
|
|
@ -102,15 +274,18 @@ export function generateTerrainAndRivers(seed) {
|
|||
0.105 * terrainLocal +
|
||||
0.055 * terrainFine +
|
||||
mountains * 0.54 +
|
||||
ridges * 1.22 +
|
||||
spineRidges * 0.78 +
|
||||
branchRidges * 0.92 +
|
||||
basin +
|
||||
fineDissection -
|
||||
coastLower * (coastStrength + 0.19) +
|
||||
coastLower * (coastStrength + 0.10 + terrainTemplate.deposition * 0.10) +
|
||||
0.055;
|
||||
|
||||
elevation[i] = clamp(0.5 + (rawElevation - 0.5) * 1.26);
|
||||
ridgeField[i] = clamp(ridges * 4.8 + Math.max(0, mountains - 0.10) * 0.95 + fineDissection * 2.0);
|
||||
basinField[i] = clamp(Math.max(0, -basin) * 3.0 + (1 - coastLower) * Math.max(0, 0.42 - elevation[i]) * 0.7);
|
||||
arcSpineField[i] = clamp(spineRidges * 3.7);
|
||||
branchRidgeField[i] = clamp(branchRidges * 3.9);
|
||||
ridgeField[i] = clamp(arcSpineField[i] * 0.86 + branchRidgeField[i] * 0.72 + Math.max(0, mountains - 0.10) * 0.95 + fineDissection * 2.0);
|
||||
basinField[i] = clamp(Math.max(0, -basin) * 3.0 + (1 - coastLower) * Math.max(0, 0.42 - elevation[i]) * (0.48 + terrainTemplate.deposition * 0.42));
|
||||
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.28 * coastLower - Math.max(0, elevation[i] - 0.62) * 0.22);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,16 +293,106 @@ export function generateTerrainAndRivers(seed) {
|
|||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const nx = x / (MAP_W - 1) - 0.5;
|
||||
const ny = y / (MAP_H - 1) - 0.5;
|
||||
const directionalCoast = nx * coastX + ny * coastY;
|
||||
const coastNoise = (fbm(x * 0.95, y * 0.95, seed + 2222) - 0.5) * 0.14 + (valueNoise(x, y, seed + 2233, 13) - 0.5) * 0.08;
|
||||
const oceanSide = directionalCoast + coastNoise > coastThreshold + 0.055;
|
||||
const coast = coastPressureAt(x, y);
|
||||
const mountainToSea = ridgeField[i] * (1 - terrainTemplate.deposition) * 0.035;
|
||||
const oceanSide = coast.pressure + mountainToSea > 0.56 + terrainTemplate.deposition * 0.035;
|
||||
if (elevation[i] < seaLevel || oceanSide) sea[i] = 1;
|
||||
if (sea[i]) elevation[i] = Math.min(elevation[i], seaLevel - 0.018 + hash2(x, y, seed + 2311) * 0.012);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Align coastal elevation with the sea mask. This prevents artificial one-cell cliffs
|
||||
// when the directional coastline cuts through a high terrain cell.
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
|
|
@ -135,18 +400,25 @@ export function generateTerrainAndRivers(seed) {
|
|||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
let nearestSea = INF;
|
||||
let nearestOcean = INF;
|
||||
for (let dy = -7; dy <= 7; dy++) {
|
||||
for (let dx = -7; dx <= 7; 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 <= 7) {
|
||||
const coastalCap = seaLevel + 0.018 + nearestSea * 0.028 + Math.max(0, fbm(x * 1.4, y * 1.4, seed + 2350) - 0.5) * 0.022;
|
||||
const coastalCap = seaLevel + 0.018 + nearestSea * (0.022 + terrainTemplate.deposition * 0.012) + Math.max(0, fbm(x * 1.4, y * 1.4, seed + 2350) - 0.5) * (0.014 + terrainTemplate.coastRoughness * 0.018);
|
||||
elevation[i] = Math.min(elevation[i], coastalCap);
|
||||
coastalLowland[i] = clamp(1 - nearestSea / 7);
|
||||
if (nearestOcean <= 7) {
|
||||
const coast = coastPressureAt(x, y);
|
||||
const side = coast.sideA >= coast.sideB ? terrainTemplate.coastSides[0] : terrainTemplate.coastSides[1];
|
||||
const plainReach = clamp(4.5 + side.plainWidth * 34, 5, 9);
|
||||
coastalLowland[i] = clamp((1 - nearestOcean / plainReach) * (0.62 + terrainTemplate.deposition * 0.48 + side.plainWidth * 1.9) * (1 - ridgeField[i] * 0.35));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -217,11 +489,12 @@ export function generateTerrainAndRivers(seed) {
|
|||
if (sea[i]) continue;
|
||||
const flow = Math.pow(flowAccum[i], 0.46);
|
||||
const incisionNoise = 0.82 + hash2(x, y, seed + 8120) * 0.36;
|
||||
const steepValley = clamp(flow * (0.058 + slope[i] * 0.21 + ridgeField[i] * 0.046) * incisionNoise);
|
||||
const lateralCut = clamp(Math.pow(flowAccum[i], 0.66) * valleyField[i] * 0.078);
|
||||
const lowSettling = clamp(flow * (coastalLowland[i] * 0.036 + basinField[i] * 0.020 + (elevation[i] < 0.40 ? 0.012 : 0)) * (1 - slope[i] * 0.82));
|
||||
const steepValley = clamp(flow * (0.036 + terrainTemplate.erosion * 0.050 + slope[i] * (0.14 + terrainTemplate.erosion * 0.13) + ridgeField[i] * (0.022 + terrainTemplate.erosion * 0.044)) * incisionNoise);
|
||||
const lateralCut = clamp(Math.pow(flowAccum[i], 0.66) * valleyField[i] * (0.044 + terrainTemplate.erosion * 0.064));
|
||||
const lowSettling = clamp(flow * (coastalLowland[i] * (0.018 + terrainTemplate.deposition * 0.040) + basinField[i] * (0.010 + terrainTemplate.deposition * 0.028) + (elevation[i] < 0.40 ? 0.006 + terrainTemplate.deposition * 0.018 : 0)) * (1 - slope[i] * 0.82) * (1 - ridgeField[i] * 0.45));
|
||||
erosionField[i] = steepValley + lateralCut;
|
||||
depositionField[i] = lowSettling;
|
||||
depositionalLowland[i] = clamp(lowSettling * 6.5 + basinField[i] * terrainTemplate.deposition * 0.28 + coastalLowland[i] * terrainTemplate.deposition * 0.34);
|
||||
shapedElevation[i] = clamp(elevation[i] - steepValley - lateralCut + lowSettling * 0.72, seaLevel + 0.006, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,8 +517,8 @@ export function generateTerrainAndRivers(seed) {
|
|||
for (let x = 4; x < MAP_W - 4; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const score = elevation[i] * 0.38 + moisture[i] * 0.24 + ridgeField[i] * 0.08 + flowAccum[i] * 0.56 + valleyField[i] * 0.28 + hash2(x, y, seed + 9000) * 0.06;
|
||||
if (elevation[i] > 0.40 && elevation[i] < 0.82 && moisture[i] > 0.28 && flowAccum[i] > 0.020 && ridgeField[i] < 0.88) sourceCandidates.push({ x, y, score });
|
||||
const score = elevation[i] * 0.38 + moisture[i] * 0.24 + ridgeField[i] * 0.08 + arcSpineField[i] * 0.07 + branchRidgeField[i] * 0.04 + flowAccum[i] * 0.56 + valleyField[i] * 0.28 + hash2(x, y, seed + 9000) * 0.06;
|
||||
if (elevation[i] > 0.40 && elevation[i] < 0.84 && moisture[i] > 0.28 && flowAccum[i] > 0.020 && ridgeField[i] < 0.95) sourceCandidates.push({ x, y, score });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -595,11 +868,12 @@ export function generateTerrainAndRivers(seed) {
|
|||
const i = indexOf(x, y);
|
||||
if (sea[i] || river[i] <= 0.02) continue;
|
||||
const r = clamp(river[i] / 3.4);
|
||||
const channelCut = clamp(Math.pow(r, 0.55) * (0.060 + slope[i] * 0.145 + ridgeField[i] * 0.038));
|
||||
const valleyWiden = clamp(Math.pow(r, 0.72) * (0.020 + Math.max(0, elevation[i] - seaLevel) * 0.058 + valleyField[i] * 0.040));
|
||||
const alluvium = clamp(Math.pow(r, 0.72) * (coastalLowland[i] * 0.030 + basinField[i] * 0.020 + (slope[i] < 0.10 ? 0.010 : 0)));
|
||||
const channelCut = clamp(Math.pow(r, 0.55) * (0.034 + terrainTemplate.erosion * 0.052 + slope[i] * (0.075 + terrainTemplate.erosion * 0.120) + ridgeField[i] * (0.018 + terrainTemplate.erosion * 0.048)));
|
||||
const valleyWiden = clamp(Math.pow(r, 0.72) * (0.012 + terrainTemplate.erosion * 0.026 + Math.max(0, elevation[i] - seaLevel) * (0.030 + terrainTemplate.erosion * 0.050) + valleyField[i] * (0.020 + terrainTemplate.erosion * 0.045)));
|
||||
const alluvium = clamp(Math.pow(r, 0.72) * (coastalLowland[i] * (0.014 + terrainTemplate.deposition * 0.040) + basinField[i] * (0.010 + terrainTemplate.deposition * 0.028) + (slope[i] < 0.10 ? 0.006 + terrainTemplate.deposition * 0.018 : 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.62 + channelCut * 6.4);
|
||||
basinField[i] = clamp(basinField[i] + alluvium * 3.2);
|
||||
|
|
@ -622,7 +896,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius || d === 0) continue;
|
||||
const weight = (radius + 0.35 - d) / (radius + 0.35);
|
||||
const carve = Math.max(0, weight) * (0.008 + r * 0.026) * Math.max(0.45, slope[ni] + 0.22);
|
||||
const carve = Math.max(0, weight) * (0.005 + terrainTemplate.erosion * 0.007 + r * (0.014 + terrainTemplate.erosion * 0.022)) * Math.max(0.45, slope[ni] + 0.22);
|
||||
fluvialElevation[ni] = clamp(fluvialElevation[ni] - carve, seaLevel + 0.005, 1);
|
||||
erosionField[ni] = clamp(erosionField[ni] + carve * 3.0);
|
||||
valleyField[ni] = clamp(valleyField[ni] + carve * 12.0);
|
||||
|
|
@ -631,6 +905,57 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
|
|
@ -654,7 +979,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
// 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; pass++) {
|
||||
for (let pass = 0; pass < 3 + Math.round(terrainTemplate.deposition * 2); pass++) {
|
||||
const nextElevation = new Float32Array(elevation);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
|
|
@ -663,6 +988,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
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.34 +
|
||||
Math.pow(flowAccum[i], 0.58) * 0.24 -
|
||||
ridgeField[i] * 0.62 -
|
||||
|
|
@ -690,9 +1018,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
const localMean = sum / weight;
|
||||
const terrace = Math.round(localMean * 42) / 42;
|
||||
const target = lerp(localMean, terrace, 0.28);
|
||||
nextElevation[i] = clamp(lerp(elevation[i], target, lowland * 0.42), seaLevel + 0.006, 1);
|
||||
nextElevation[i] = clamp(lerp(elevation[i], target, lowland * (0.30 + terrainTemplate.deposition * 0.26)), seaLevel + 0.006, 1);
|
||||
if (lowland > 0.55) {
|
||||
depositionField[i] = clamp(depositionField[i] + lowland * 0.018);
|
||||
depositionField[i] = clamp(depositionField[i] + lowland * (0.010 + terrainTemplate.deposition * 0.018));
|
||||
erosionField[i] = Math.max(0, erosionField[i] - lowland * 0.012);
|
||||
}
|
||||
}
|
||||
|
|
@ -785,7 +1113,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
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;
|
||||
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;
|
||||
|
|
@ -798,9 +1126,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
const fan = clamp(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);
|
||||
agriculture[i] = clamp(plain[i] * 0.58 + fan * 0.26 + basinField[i] * 0.2 + moisture[i] * 0.14 + clamp(nearRiver) * 0.32 - slope[i] * 0.34 - ridgeField[i] * 0.18 - floodplain[i] * 0.06);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -831,9 +1159,9 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
const isDelta = riverNear > 0.22 && coastalLowland[i] > 0.18;
|
||||
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) + plain[i] * 0.08 - slope[i] * 0.48 - ridgeField[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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -867,12 +1195,39 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
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 = 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
terrainTemplate,
|
||||
elevation,
|
||||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
ocean,
|
||||
lake,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
|
|
@ -884,6 +1239,12 @@ export function generateTerrainAndRivers(seed) {
|
|||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
arcSpineField,
|
||||
branchRidgeField,
|
||||
depositionalLowland,
|
||||
alluvialFanField,
|
||||
deltaField,
|
||||
naturalBarrierScore,
|
||||
portSuitability,
|
||||
crossingSuitability,
|
||||
passSuitability,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue