hmm?
This commit is contained in:
parent
ea0067dc0a
commit
112e6bf86b
11 changed files with 1586 additions and 1253 deletions
645
mapTerrain.js
645
mapTerrain.js
|
|
@ -5,6 +5,7 @@ import {
|
|||
neighbors8,
|
||||
} from "./mapGeneratorHelpers.js";
|
||||
import { buildNaturalCompartments } from "./adminRegions.js";
|
||||
import { createRectContext, createRectTerrainFields, rectIndexOf, rectInside, rectNeighbors8, rectQuantile } from "./rectContext.js";
|
||||
|
||||
const ASPECT = MAP_W / MAP_H;
|
||||
const SQRT2 = Math.SQRT2;
|
||||
|
|
@ -1170,6 +1171,650 @@ function enforceLandGradient(elevation, sea, seaLevel) {
|
|||
}
|
||||
}
|
||||
|
||||
function rectTerrainProfile(template) {
|
||||
const id = String(template?.terrainType || "auto");
|
||||
if (id.includes("oceanic")) return {
|
||||
base: 0.36, relief: 0.19, ridge: 0.30, ridgeWidth: 22, ridgeSpacing: 76, coast: 0.34, archipelago: 0.28,
|
||||
seaQuantile: Math.max(0.68, template.seaRatio ?? 0.76), plain: 0.20, moisture: 0.60, capStart: 0.64, capMax: 0.86,
|
||||
};
|
||||
if (id.includes("setouchi") || id.includes("archipelago")) return {
|
||||
base: 0.43, relief: 0.18, ridge: 0.34, ridgeWidth: 30, ridgeSpacing: 94, coast: 0.25, archipelago: 0.20,
|
||||
seaQuantile: Math.max(0.30, template.seaRatio ?? 0.36), plain: 0.34, moisture: 0.58, capStart: 0.72, capMax: 0.94,
|
||||
};
|
||||
if (id.includes("chubu") || id.includes("mountain")) return {
|
||||
base: 0.52, relief: 0.26, ridge: 0.62, ridgeWidth: 36, ridgeSpacing: 108, coast: 0.12, archipelago: 0.03,
|
||||
seaQuantile: Math.min(0.22, template.seaRatio ?? 0.18), plain: 0.15, moisture: 0.48, capStart: 0.90, capMax: 1.10,
|
||||
};
|
||||
if (id.includes("kanto") || id.includes("alluvial")) return {
|
||||
base: 0.48, relief: 0.12, ridge: 0.18, ridgeWidth: 42, ridgeSpacing: 130, coast: 0.16, archipelago: 0.04,
|
||||
seaQuantile: template.seaRatio ?? 0.13, plain: 0.66, moisture: 0.56, capStart: 0.84, capMax: 1.00,
|
||||
};
|
||||
if (id.includes("tohoku") || id.includes("spine")) return {
|
||||
base: 0.49, relief: 0.19, ridge: 0.46, ridgeWidth: 24, ridgeSpacing: 88, coast: 0.18, archipelago: 0.03,
|
||||
seaQuantile: template.seaRatio ?? 0.22, plain: 0.26, moisture: 0.52, capStart: 0.78, capMax: 0.98,
|
||||
};
|
||||
return {
|
||||
base: 0.45, relief: 0.18, ridge: 0.34, ridgeWidth: 32, ridgeSpacing: 100, coast: 0.18, archipelago: 0.06,
|
||||
seaQuantile: template.seaRatio ?? 0.20, plain: 0.30, moisture: 0.52, capStart: 0.86, capMax: 1.04,
|
||||
};
|
||||
}
|
||||
|
||||
function rectSeed(seed, variant, salt) {
|
||||
let h = (seed >>> 0) ^ Math.imul((variant || 0) >>> 0, 0x9e3779b9) ^ (salt >>> 0);
|
||||
h ^= h >>> 16;
|
||||
h = Math.imul(h, 0x7feb352d) >>> 0;
|
||||
h ^= h >>> 15;
|
||||
h = Math.imul(h, 0x846ca68b) >>> 0;
|
||||
return (h ^ (h >>> 16)) >>> 0;
|
||||
}
|
||||
|
||||
function periodicRidgeField(wx, wy, template, profile, seed) {
|
||||
const angle = template.mountainAngle || 0;
|
||||
const c = Math.cos(angle);
|
||||
const s = Math.sin(angle);
|
||||
const u = wx * c + wy * s;
|
||||
const v = -wx * s + wy * c;
|
||||
const spacing = Math.max(18, profile.ridgeSpacing);
|
||||
const shifted = v / spacing + valueNoise(wx, wy, seed ^ 0x654f6d23, 115) * 0.70;
|
||||
const nearest = Math.abs((shifted - Math.round(shifted)) * spacing);
|
||||
const ridgeCore = Math.exp(-Math.pow(nearest / Math.max(4, profile.ridgeWidth), 2.0));
|
||||
const along = valueNoise(u, v, seed ^ 0x27d4eb2f, 86);
|
||||
const cut = valueNoise(u, v, seed ^ 0x165667b1, 31);
|
||||
return clamp(ridgeCore * (0.62 + along * 0.62) * (0.74 + cut * 0.40));
|
||||
}
|
||||
|
||||
function worldMarinePressure(wx, wy, template, profile, seed) {
|
||||
const angle = template.coastAngle || 0;
|
||||
const c = Math.cos(angle);
|
||||
const s = Math.sin(angle);
|
||||
const axis = wx * c + wy * s;
|
||||
const cross = -wx * s + wy * c;
|
||||
const period = template.coastStyle === "oceanic_archipelago" ? 160 : template.coastStyle === "inland_sea" ? 220 : 300;
|
||||
const broad = Math.sin((axis + valueNoise(wx, wy, seed ^ 0xc2b2ae35, 190) * 90) / period * Math.PI * 2);
|
||||
const channel = Math.exp(-Math.pow((cross + (valueNoise(wx, wy, seed ^ 0x85ebca6b, 130) - 0.5) * 80) / (profile.ridgeSpacing * 0.85), 2.0));
|
||||
const radial = valueNoise(wx, wy, seed ^ 0x9e3779b9, 260);
|
||||
let pressure = clamp((broad * 0.5 + 0.5) * profile.coast + channel * profile.coast * 0.62 + radial * profile.coast * 0.52);
|
||||
if (template.coastStyle === "oceanic_archipelago") {
|
||||
const gap = clamp((fbm(wx * 0.75 + 33, wy * 0.75 - 17, seed ^ 0x3c6ef372) - 0.42) * 2.2);
|
||||
pressure = clamp(pressure + gap * profile.archipelago);
|
||||
}
|
||||
if (template.coastStyle === "open_bay") pressure = clamp(pressure + channel * 0.12);
|
||||
return pressure;
|
||||
}
|
||||
|
||||
function classifyRectWater(ctx, fields, seaLevel) {
|
||||
const { elevation, sea, ocean, lake } = fields;
|
||||
sea.fill(0); ocean.fill(0); lake.fill(0);
|
||||
const water = new Uint8Array(ctx.size);
|
||||
for (let i = 0; i < ctx.size; i++) water[i] = elevation[i] <= seaLevel ? 1 : 0;
|
||||
const seen = new Uint8Array(ctx.size);
|
||||
let oceanCells = 0;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (!water[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 % ctx.width;
|
||||
const y = Math.floor(cur / ctx.width);
|
||||
if (x === 0 || y === 0 || x === ctx.width - 1 || y === ctx.height - 1) touchesEdge = true;
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, nx, ny);
|
||||
if (!water[ni] || seen[ni]) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
const isOcean = touchesEdge || cells.length > Math.max(96, ctx.size * 0.018);
|
||||
if (isOcean || cells.length >= 20) {
|
||||
for (const ci of cells) {
|
||||
sea[ci] = 1;
|
||||
if (isOcean) ocean[ci] = 1;
|
||||
else lake[ci] = 1;
|
||||
}
|
||||
if (isOcean) oceanCells += cells.length;
|
||||
} else {
|
||||
for (const ci of cells) elevation[ci] = seaLevel + 0.012;
|
||||
}
|
||||
}
|
||||
return oceanCells;
|
||||
}
|
||||
|
||||
function recomputeRectSlope(ctx, fields) {
|
||||
const { elevation, sea, slope } = fields;
|
||||
slope.fill(0);
|
||||
for (let y = 1; y < ctx.height - 1; y++) {
|
||||
for (let x = 1; x < ctx.width - 1; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i]) continue;
|
||||
const gx = elevation[rectIndexOf(ctx, x + 1, y)] - elevation[rectIndexOf(ctx, x - 1, y)];
|
||||
const gy = elevation[rectIndexOf(ctx, x, y + 1)] - elevation[rectIndexOf(ctx, x, y - 1)];
|
||||
slope[i] = clamp(Math.hypot(gx, gy) * 8.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function priorityFloodRect(ctx, fields) {
|
||||
const { elevation, sea, flowTo } = fields;
|
||||
const filled = new Float32Array(elevation);
|
||||
const visited = new Uint8Array(ctx.size);
|
||||
const heap = new MinHeap();
|
||||
let seeds = 0;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
const x = i % ctx.width;
|
||||
const y = Math.floor(i / ctx.width);
|
||||
if (sea[i] || x === 0 || y === 0 || x === ctx.width - 1 || y === ctx.height - 1) {
|
||||
visited[i] = 1;
|
||||
heap.push({ i, f: filled[i] });
|
||||
seeds++;
|
||||
}
|
||||
}
|
||||
if (!seeds) return filled;
|
||||
while (heap.length) {
|
||||
const cur = heap.pop();
|
||||
if (!cur || cur.f > filled[cur.i] + 1e-5) continue;
|
||||
const x = cur.i % ctx.width;
|
||||
const y = Math.floor(cur.i / ctx.width);
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, 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] });
|
||||
}
|
||||
}
|
||||
flowTo.fill(-1);
|
||||
for (let y = 0; y < ctx.height; y++) {
|
||||
for (let x = 0; x < ctx.width; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i]) continue;
|
||||
let best = -1;
|
||||
let bestScore = filled[i];
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, nx, ny);
|
||||
const stepPenalty = (nx !== x && ny !== y) ? 0.000015 : 0;
|
||||
const score = filled[ni] + stepPenalty + hash2(ctx.originX + nx, ctx.originY + ny, 9000) * 0.000002;
|
||||
if (score < bestScore - 0.000001 || sea[ni]) {
|
||||
bestScore = score;
|
||||
best = ni;
|
||||
if (sea[ni]) break;
|
||||
}
|
||||
}
|
||||
flowTo[i] = best;
|
||||
}
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
function computeRectFlowAccumulation(ctx, fields, filled) {
|
||||
const { sea, flowTo, flowAccum } = fields;
|
||||
const area = new Float32Array(ctx.size);
|
||||
const order = [];
|
||||
for (let i = 0; i < ctx.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 < ctx.size; i++) if (!sea[i]) maxArea = Math.max(maxArea, area[i]);
|
||||
for (let i = 0; i < ctx.size; i++) flowAccum[i] = sea[i] ? 0 : clamp(Math.pow(area[i] / maxArea, 0.42));
|
||||
}
|
||||
|
||||
function rectStableId(seed, wx, wy, salt) {
|
||||
const x = Math.floor(wx) | 0;
|
||||
const y = Math.floor(wy) | 0;
|
||||
let h = (seed >>> 0) ^ Math.imul(x, 0x9e3779b1) ^ Math.imul(y, 0x85ebca77) ^ (salt >>> 0);
|
||||
h ^= h >>> 16;
|
||||
h = Math.imul(h, 0x7feb352d) >>> 0;
|
||||
h ^= h >>> 15;
|
||||
h = Math.imul(h, 0x846ca68b) >>> 0;
|
||||
return (h ^ (h >>> 16)) & 0x7fffffff;
|
||||
}
|
||||
|
||||
function traceRectSink(ctx, start, fields, maxSteps = 4096) {
|
||||
const { sea, flowTo } = fields;
|
||||
let i = start;
|
||||
let last = i;
|
||||
const seen = new Set();
|
||||
for (let step = 0; step < maxSteps; step++) {
|
||||
if (i < 0 || i >= ctx.size || seen.has(i)) break;
|
||||
seen.add(i);
|
||||
last = i;
|
||||
if (sea[i]) break;
|
||||
const next = flowTo[i];
|
||||
if (next < 0 || next === i) break;
|
||||
i = next;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
function buildRectWatershedId(ctx, fields, seed) {
|
||||
const { sea, flowAccum, watershedId } = fields;
|
||||
if (!watershedId) return { watershedCount: 0 };
|
||||
watershedId.fill(-1);
|
||||
const sinkToId = new Map();
|
||||
let watershedCount = 0;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (sea[i]) continue;
|
||||
const sink = traceRectSink(ctx, i, fields);
|
||||
const sx = sink % ctx.width;
|
||||
const sy = Math.floor(sink / ctx.width);
|
||||
const wx = ctx.originX + sx;
|
||||
const wy = ctx.originY + sy;
|
||||
const coarseX = Math.round(wx / 12);
|
||||
const coarseY = Math.round(wy / 12);
|
||||
const key = `${coarseX},${coarseY}`;
|
||||
let id = sinkToId.get(key);
|
||||
if (!Number.isFinite(id)) {
|
||||
id = 50000000 + rectStableId(seed, coarseX, coarseY, 0x51ed270b) % 40000000;
|
||||
sinkToId.set(key, id);
|
||||
watershedCount++;
|
||||
}
|
||||
watershedId[i] = id;
|
||||
}
|
||||
// Merge tiny or noisy drainage islands into their strongest neighbor.
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
const changes = [];
|
||||
for (let y = 1; y < ctx.height - 1; y++) {
|
||||
for (let x = 1; x < ctx.width - 1; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i] || watershedId[i] < 0) continue;
|
||||
const counts = new Map();
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, nx, ny);
|
||||
const id = watershedId[ni];
|
||||
if (id < 0) continue;
|
||||
counts.set(id, (counts.get(id) || 0) + 1 + (flowAccum[ni] || 0));
|
||||
}
|
||||
let best = watershedId[i];
|
||||
let bestScore = counts.get(best) || 0;
|
||||
for (const [id, score] of counts) if (score > bestScore) { best = id; bestScore = score; }
|
||||
if (best !== watershedId[i] && bestScore >= 5.5) changes.push([i, best]);
|
||||
}
|
||||
}
|
||||
for (const [i, id] of changes) watershedId[i] = id;
|
||||
if (!changes.length) break;
|
||||
}
|
||||
return { watershedCount };
|
||||
}
|
||||
|
||||
function buildRectNaturalRegions(ctx, fields, seed, template) {
|
||||
const { sea, ridgeField, valleyField, basinField, flowAccum, naturalBarrierScore, watershedId, naturalCompartmentId, regionId } = fields;
|
||||
if (!naturalCompartmentId || !regionId) return { naturalCompartmentCount: 0, regionCount: 0 };
|
||||
naturalCompartmentId.fill(-1);
|
||||
regionId.fill(-1);
|
||||
const type = String(template?.terrainType || "auto");
|
||||
const spacing = type.includes("oceanic") ? 30 : type.includes("kanto") ? 44 : type.includes("chubu") ? 34 : 38;
|
||||
const coarseSpacing = spacing * 2.55;
|
||||
const seeds = [];
|
||||
const gx0 = Math.floor((ctx.originX - spacing) / spacing) - 1;
|
||||
const gx1 = Math.ceil((ctx.originX + ctx.width + spacing) / spacing) + 1;
|
||||
const gy0 = Math.floor((ctx.originY - spacing) / spacing) - 1;
|
||||
const gy1 = Math.ceil((ctx.originY + ctx.height + spacing) / spacing) + 1;
|
||||
for (let gy = gy0; gy <= gy1; gy++) {
|
||||
for (let gx = gx0; gx <= gx1; gx++) {
|
||||
const jitterX = (hash2(gx, gy, seed ^ 0x6a09e667) - 0.5) * spacing * 0.74;
|
||||
const jitterY = (hash2(gx, gy, seed ^ 0xbb67ae85) - 0.5) * spacing * 0.74;
|
||||
const wx = gx * spacing + spacing * 0.5 + jitterX;
|
||||
const wy = gy * spacing + spacing * 0.5 + jitterY;
|
||||
const lx = Math.round(wx - ctx.originX);
|
||||
const ly = Math.round(wy - ctx.originY);
|
||||
let viability = 0.8;
|
||||
if (rectInside(ctx, lx, ly)) {
|
||||
const i = rectIndexOf(ctx, lx, ly);
|
||||
viability += (basinField[i] || 0) * 0.25 + (valleyField[i] || 0) * 0.16 - (ridgeField[i] || 0) * 0.14;
|
||||
if (sea[i]) viability -= 1.2;
|
||||
}
|
||||
if (viability < 0.18 && hash2(gx, gy, seed ^ 0x3c6ef372) < 0.82) continue;
|
||||
seeds.push({
|
||||
wx,
|
||||
wy,
|
||||
id: 40000000 + rectStableId(seed, gx, gy, 0xb5c0fbcf) % 42000000,
|
||||
coarseId: 30000000 + rectStableId(seed, Math.floor((gx * spacing) / coarseSpacing), Math.floor((gy * spacing) / coarseSpacing), 0xc2b2ae35) % 42000000,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!seeds.length) return { naturalCompartmentCount: 0, regionCount: 0 };
|
||||
for (let y = 0; y < ctx.height; y++) {
|
||||
for (let x = 0; x < ctx.width; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i]) continue;
|
||||
const wx = ctx.originX + x;
|
||||
const wy = ctx.originY + y;
|
||||
let best = seeds[0];
|
||||
let bestScore = Infinity;
|
||||
const barrier = (naturalBarrierScore[i] || 0) + (ridgeField[i] || 0) * 0.55 + (flowAccum[i] || 0) * 0.18;
|
||||
const basinBonus = (basinField[i] || 0) * 0.18 + (valleyField[i] || 0) * 0.10;
|
||||
for (const s of seeds) {
|
||||
const dx = (wx - s.wx) * 1.05;
|
||||
const dy = wy - s.wy;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const tileNoise = (valueNoise(wx + s.wx * 0.13, wy + s.wy * 0.13, seed ^ 0xa54ff53a, 52) - 0.5) * spacing * 0.34;
|
||||
const watershedPenalty = watershedId?.[i] >= 0 ? ((watershedId[i] ^ s.id) & 7) * 0.16 : 0;
|
||||
const score = d + barrier * spacing * 0.42 - basinBonus * spacing * 0.32 + tileNoise + watershedPenalty;
|
||||
if (score < bestScore) { bestScore = score; best = s; }
|
||||
}
|
||||
naturalCompartmentId[i] = best.id;
|
||||
regionId[i] = best.coarseId;
|
||||
}
|
||||
}
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
const changes = [];
|
||||
for (let y = 1; y < ctx.height - 1; y++) {
|
||||
for (let x = 1; x < ctx.width - 1; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i]) continue;
|
||||
if ((ridgeField[i] || 0) > 0.78) continue;
|
||||
const counts = new Map();
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, nx, ny);
|
||||
const id = naturalCompartmentId[ni];
|
||||
if (id < 0) continue;
|
||||
counts.set(id, (counts.get(id) || 0) + 1 + (basinField[ni] || 0) * 0.3);
|
||||
}
|
||||
let best = naturalCompartmentId[i];
|
||||
let bestScore = counts.get(best) || 0;
|
||||
for (const [id, score] of counts) if (score > bestScore) { best = id; bestScore = score; }
|
||||
if (best !== naturalCompartmentId[i] && bestScore >= 5.8) changes.push([i, best]);
|
||||
}
|
||||
}
|
||||
for (const [i, id] of changes) naturalCompartmentId[i] = id;
|
||||
if (!changes.length) break;
|
||||
}
|
||||
const nset = new Set();
|
||||
const rset = new Set();
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (naturalCompartmentId[i] >= 0) nset.add(naturalCompartmentId[i]);
|
||||
if (regionId[i] >= 0) rset.add(regionId[i]);
|
||||
}
|
||||
return { naturalCompartmentCount: nset.size, regionCount: rset.size };
|
||||
}
|
||||
|
||||
function traceRectFlowPath(start, ctx, fields, maxSteps = 1200) {
|
||||
const { sea, flowTo } = fields;
|
||||
let i = start;
|
||||
const path = [];
|
||||
const seen = new Set();
|
||||
for (let step = 0; step < maxSteps; step++) {
|
||||
if (i < 0 || i >= ctx.size || seen.has(i)) break;
|
||||
seen.add(i);
|
||||
const x = i % ctx.width;
|
||||
const y = Math.floor(i / ctx.width);
|
||||
path.push([ctx.originX + x, ctx.originY + y]);
|
||||
if (sea[i]) break;
|
||||
const next = flowTo[i];
|
||||
if (next < 0 || next === i) break;
|
||||
i = next;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function scoreRectRiverPath(path, ctx, fields) {
|
||||
let score = 0;
|
||||
for (const [wx, wy] of path) {
|
||||
const x = wx - ctx.originX;
|
||||
const y = wy - ctx.originY;
|
||||
if (!rectInside(ctx, x, y)) continue;
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
score += (fields.flowAccum[i] || 0) + (fields.river[i] || 0) * 0.7;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function buildRectRiverPaths(ctx, fields, seed, template) {
|
||||
const { sea, flowAccum, river, erosionField } = fields;
|
||||
const candidates = [];
|
||||
const threshold = template?.terrainType === "oceanic_archipelago" ? 0.52 : template?.terrainType === "kanto_alluvial" ? 0.46 : 0.50;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (sea[i] || flowAccum[i] < threshold) continue;
|
||||
const x = i % ctx.width;
|
||||
const y = Math.floor(i / ctx.width);
|
||||
let upstream = 0;
|
||||
for (const [nx, ny] of rectNeighbors8(ctx, x, y)) {
|
||||
const ni = rectIndexOf(ctx, nx, ny);
|
||||
if (fields.flowTo[ni] === i) upstream++;
|
||||
}
|
||||
const sourceBias = hash2(ctx.originX + x, ctx.originY + y, seed ^ 0x1f123bb5);
|
||||
if (upstream <= 1 || sourceBias > 0.78) candidates.push({ i, score: flowAccum[i] + sourceBias * 0.12 });
|
||||
}
|
||||
candidates.sort((a, b) => b.score - a.score);
|
||||
const accepted = [];
|
||||
const occupied = new Set();
|
||||
const desired = Math.min(72, Math.max(8, Math.floor(ctx.size / 900)));
|
||||
for (const c of candidates) {
|
||||
if (accepted.length >= desired) break;
|
||||
const path = traceRectFlowPath(c.i, ctx, fields);
|
||||
if (path.length < 8) continue;
|
||||
const keyHits = path.reduce((n, [wx, wy], k) => k % 3 === 0 && occupied.has(`${wx},${wy}`) ? n + 1 : n, 0);
|
||||
if (keyHits > Math.max(5, path.length * 0.18)) continue;
|
||||
const score = scoreRectRiverPath(path, ctx, fields);
|
||||
if (score < 4.2) continue;
|
||||
accepted.push({ path, score });
|
||||
for (const [wx, wy] of path) occupied.add(`${wx},${wy}`);
|
||||
}
|
||||
accepted.sort((a, b) => b.score - a.score);
|
||||
const mainRivers = accepted.slice(0, Math.max(1, Math.min(10, Math.round(accepted.length * 0.25)))).map((r) => r.path);
|
||||
const tributaryRivers = accepted.slice(mainRivers.length, mainRivers.length + 28).map((r) => r.path);
|
||||
const smallStreams = accepted.slice(mainRivers.length + 28, mainRivers.length + 56).map((r) => r.path);
|
||||
for (const group of [mainRivers, tributaryRivers, smallStreams]) {
|
||||
const boost = group === mainRivers ? 0.72 : group === tributaryRivers ? 0.48 : 0.28;
|
||||
for (const path of group) {
|
||||
for (const [wx, wy] of path) {
|
||||
const x = wx - ctx.originX;
|
||||
const y = wy - ctx.originY;
|
||||
if (!rectInside(ctx, x, y)) continue;
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
if (sea[i]) continue;
|
||||
river[i] = clamp(Math.max(river[i], boost + (flowAccum[i] || 0) * 0.42));
|
||||
if (erosionField) erosionField[i] = clamp((erosionField[i] || 0) + river[i] * 0.18);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { riverPaths: accepted.map((r) => r.path), mainRivers, tributaryRivers, smallStreams };
|
||||
}
|
||||
|
||||
function deriveRectTerrainFields(ctx, fields, seaLevel) {
|
||||
const {
|
||||
elevation, sea, river, flowAccum, floodplain, plain, agriculture, ridgeField, valleyField, basinField,
|
||||
coastalLowland, erosionField, depositionField, depositionalLowland, alluvialFanField, deltaField,
|
||||
naturalBarrierScore, portSuitability, crossingSuitability, passSuitability, slope, moisture,
|
||||
} = fields;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (sea[i]) {
|
||||
river[i] = 0; plain[i] = 0; agriculture[i] = 0; naturalBarrierScore[i] = 0;
|
||||
continue;
|
||||
}
|
||||
const low = clamp((0.48 - elevation[i]) * 2.2);
|
||||
const flat = clamp(1 - slope[i] * 2.3);
|
||||
const coast = clamp((elevation[i] - seaLevel) * 18);
|
||||
river[i] = flowAccum[i] > 0.58 ? clamp((flowAccum[i] - 0.52) * 2.1 + (0.22 - slope[i]) * 0.40) : 0;
|
||||
floodplain[i] = clamp(river[i] * 0.72 + low * flat * 0.24);
|
||||
plain[i] = clamp(flat * (low * 0.78 + basinField[i] * 0.38 + floodplain[i] * 0.35));
|
||||
agriculture[i] = clamp(plain[i] * 0.72 + moisture[i] * 0.22 - slope[i] * 0.22);
|
||||
coastalLowland[i] = clamp((1 - coast) * flat * 0.90);
|
||||
erosionField[i] = clamp(slope[i] * 0.55 + river[i] * 0.34 + ridgeField[i] * 0.22);
|
||||
depositionField[i] = clamp(floodplain[i] * 0.58 + coastalLowland[i] * 0.34 + plain[i] * 0.18);
|
||||
depositionalLowland[i] = clamp(depositionField[i] * flat);
|
||||
alluvialFanField[i] = clamp(river[i] * slope[i] * 1.8);
|
||||
deltaField[i] = clamp(river[i] * coastalLowland[i] * 1.2);
|
||||
naturalBarrierScore[i] = clamp(ridgeField[i] * 0.72 + slope[i] * 0.42 + river[i] * 0.24);
|
||||
crossingSuitability[i] = clamp(flat * (1 - river[i] * 0.65) + plain[i] * 0.24);
|
||||
passSuitability[i] = clamp((1 - ridgeField[i]) * 0.55 + valleyField[i] * 0.40 - slope[i] * 0.15);
|
||||
portSuitability[i] = clamp(coastalLowland[i] * 0.65 + plain[i] * 0.22 - slope[i] * 0.26);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateTerrainRect(options = {}) {
|
||||
const seed = Number.isFinite(options.seed) ? options.seed >>> 0 : 0;
|
||||
const variant = Number.isFinite(options.variant) ? Math.max(0, Math.floor(options.variant)) >>> 0 : 0;
|
||||
const ctx = options.rectContext || createRectContext(options);
|
||||
const rectSeedValue = rectSeed(seed, variant, 0x5489a1f3);
|
||||
const terrainTemplate = buildTerrainTemplate(rectSeedValue, options);
|
||||
const profile = rectTerrainProfile(terrainTemplate);
|
||||
const fields = createRectTerrainFields(ctx);
|
||||
const {
|
||||
elevation, moisture, ridgeField, valleyField, basinField, coastalLowland, arcSpineField, branchRidgeField,
|
||||
visibleRavineField, surfaceTextureField,
|
||||
} = fields;
|
||||
|
||||
for (let y = 0; y < ctx.height; y++) {
|
||||
for (let x = 0; x < ctx.width; x++) {
|
||||
const i = rectIndexOf(ctx, x, y);
|
||||
const wx = ctx.originX + x;
|
||||
const wy = ctx.originY + y;
|
||||
const broad = (fbm(wx * 0.58, wy * 0.58, rectSeedValue ^ 0x9e3779b9) - 0.5) * profile.relief;
|
||||
const regional = (valueNoise(wx, wy, rectSeedValue ^ 0x85ebca6b, 58) - 0.5) * profile.relief * 0.72;
|
||||
const detail = (valueNoise(wx, wy, rectSeedValue ^ 0xc2b2ae35, 19) - 0.5) * profile.relief * 0.22;
|
||||
const ridge = periodicRidgeField(wx, wy, terrainTemplate, profile, rectSeedValue ^ 0x27d4eb2f);
|
||||
const marine = worldMarinePressure(wx, wy, terrainTemplate, profile, rectSeedValue ^ 0x165667b1);
|
||||
const basin = clamp((valueNoise(wx, wy, rectSeedValue ^ 0xd3a2646c, 120) - 0.36) * 1.65) * profile.plain;
|
||||
const valley = clamp((1 - ridge) * (valueNoise(wx, wy, rectSeedValue ^ 0xfd7046c5, 42) - 0.42) * 1.7);
|
||||
const archipelago = terrainTemplate.terrainType === "oceanic_archipelago" || terrainTemplate.coastStyle === "inland_sea"
|
||||
? clamp((fbm(wx * 0.72 + 49, wy * 0.72 - 31, rectSeedValue ^ 0x94d049bb) - 0.44) * 2.2) * profile.archipelago
|
||||
: 0;
|
||||
let e = profile.base + broad + regional + detail + ridge * profile.ridge + archipelago - marine + basin * 0.10;
|
||||
if (terrainTemplate.terrainType === "kanto_alluvial") e -= basin * 0.075;
|
||||
if (terrainTemplate.terrainType === "oceanic_archipelago") e -= marine * 0.16;
|
||||
e = softCapElevation(e, profile.capStart, profile.capMax);
|
||||
elevation[i] = clamp(e, 0.025, profile.capMax);
|
||||
ridgeField[i] = clamp(ridge * (0.62 + profile.ridge));
|
||||
branchRidgeField[i] = clamp(ridge * 0.82 + detail * 0.60);
|
||||
arcSpineField[i] = clamp(ridge * 0.90);
|
||||
valleyField[i] = clamp(valley + (1 - ridge) * marine * 0.20);
|
||||
basinField[i] = clamp(basin + valley * 0.35);
|
||||
coastalLowland[i] = clamp(marine * 0.82 + basin * 0.25);
|
||||
moisture[i] = clamp(profile.moisture + marine * 0.22 + basin * 0.15 - elevation[i] * 0.22 + (fbm(wx * 0.85, wy * 0.85, rectSeedValue ^ 0xa0761d65) - 0.5) * 0.13);
|
||||
visibleRavineField[i] = clamp(Math.abs(detail) * ridge * 1.9 + valley * 0.25);
|
||||
surfaceTextureField[i] = clamp(Math.abs(broad) * 0.55 + Math.abs(detail) * 1.3 + ridge * 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
const seaLevel = clamp(Number.isFinite(options.seaLevel) ? options.seaLevel : rectQuantile(elevation, profile.seaQuantile), 0.13, 0.50);
|
||||
const oceanCells = classifyRectWater(ctx, fields, seaLevel);
|
||||
recomputeRectSlope(ctx, fields);
|
||||
const filled = priorityFloodRect(ctx, fields);
|
||||
computeRectFlowAccumulation(ctx, fields, filled);
|
||||
const watershedDebug = buildRectWatershedId(ctx, fields, rectSeedValue ^ 0x51ed270b);
|
||||
deriveRectTerrainFields(ctx, fields, seaLevel);
|
||||
const riverNetwork = buildRectRiverPaths(ctx, fields, rectSeedValue ^ 0x1f123bb5, terrainTemplate);
|
||||
const naturalDebug = buildRectNaturalRegions(ctx, fields, rectSeedValue ^ 0xb5c0fbcf, terrainTemplate);
|
||||
|
||||
let landCount = 0;
|
||||
let mountainCount = 0;
|
||||
let plainCount = 0;
|
||||
for (let i = 0; i < ctx.size; i++) {
|
||||
if (fields.sea[i]) continue;
|
||||
landCount++;
|
||||
if (fields.elevation[i] > 0.56 || fields.ridgeField[i] > 0.52) mountainCount++;
|
||||
if (fields.plain[i] > 0.36) plainCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
rectContext: ctx,
|
||||
originX: ctx.originX,
|
||||
originY: ctx.originY,
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
size: ctx.size,
|
||||
terrainTemplate,
|
||||
seaLevel,
|
||||
...fields,
|
||||
terrainDebug: {
|
||||
terrainType: terrainTemplate.terrainType,
|
||||
terrainTypeLabel: terrainTemplate.terrainTypeLabel,
|
||||
coastStyle: terrainTemplate.coastStyle,
|
||||
rectNative: true,
|
||||
originX: ctx.originX,
|
||||
originY: ctx.originY,
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
variant,
|
||||
seaRatio: fields.sea.reduce((sum, value) => sum + value, 0) / Math.max(1, ctx.size),
|
||||
landCount,
|
||||
oceanCells,
|
||||
mountainRatio: mountainCount / Math.max(1, landCount),
|
||||
plainRatio: plainCount / Math.max(1, landCount),
|
||||
watershedCount: watershedDebug.watershedCount,
|
||||
naturalCompartmentCount: naturalDebug.naturalCompartmentCount,
|
||||
regionCount: naturalDebug.regionCount,
|
||||
mainRiverCount: riverNetwork.mainRivers.length,
|
||||
tributaryRiverCount: riverNetwork.tributaryRivers.length,
|
||||
smallStreamCount: riverNetwork.smallStreams.length,
|
||||
},
|
||||
...riverNetwork,
|
||||
};
|
||||
}
|
||||
|
||||
export function finalizeRectTerrainForFixedMap(seed, terrain, options = {}) {
|
||||
if (!terrain || terrain.width !== MAP_W || terrain.height !== MAP_H || terrain.size !== SIZE) {
|
||||
throw new Error(`finalizeRectTerrainForFixedMap requires ${MAP_W}x${MAP_H} terrain, got ${terrain?.width}x${terrain?.height}`);
|
||||
}
|
||||
const {
|
||||
elevation, slope, sea, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum,
|
||||
plain, agriculture, watershedId, landMask: existingLandMask, prefectureMask: existingPrefectureMask,
|
||||
} = terrain;
|
||||
const prefectureMask = existingPrefectureMask || makePrefectureMask(seed, sea, elevation, slope, river);
|
||||
const landMask = existingLandMask || new Uint8Array(SIZE);
|
||||
if (!existingLandMask) {
|
||||
for (let i = 0; i < SIZE; i++) landMask[i] = sea[i] ? 0 : 1;
|
||||
}
|
||||
const zeroDensity = new Float32Array(SIZE);
|
||||
const zeroLanduse = new Int8Array(SIZE);
|
||||
const landCount = landMask.reduce((sum, value, i) => sum + (value && !sea[i] ? 1 : 0), 0);
|
||||
const natural = buildNaturalCompartments(
|
||||
landMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum,
|
||||
null, plain, agriculture, zeroDensity, zeroLanduse,
|
||||
{
|
||||
seed: (seed + 17003) >>> 0,
|
||||
watershedId,
|
||||
targetCompartmentCount: clamp(Math.round(landCount / 45), 70, 360),
|
||||
}
|
||||
);
|
||||
const prefectureBorder = extractMaskBorder(prefectureMask, sea);
|
||||
const terrainDebug = {
|
||||
...(terrain.terrainDebug || {}),
|
||||
rectNativeInitialTerrain: true,
|
||||
rectInitialOriginX: terrain.originX || 0,
|
||||
rectInitialOriginY: terrain.originY || 0,
|
||||
sharedNaturalCompartmentLayer: true,
|
||||
naturalCompartmentCount: natural.compartments?.filter?.((unit) => unit && unit.area > 0).length || 0,
|
||||
};
|
||||
return {
|
||||
...terrain,
|
||||
prefectureMask,
|
||||
landMask,
|
||||
prefectureBorder,
|
||||
naturalBarrierScore: natural.naturalBarrierScore || terrain.naturalBarrierScore,
|
||||
naturalCompartmentId: natural.compartmentId,
|
||||
naturalCompartments: natural.compartments,
|
||||
terrainDebug,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateInitialTerrainRect(seed, options = {}) {
|
||||
const variant = Number.isFinite(options.initialVariant) ? Math.max(0, Math.floor(options.initialVariant)) : 0;
|
||||
const terrain = generateTerrainRect({
|
||||
...options,
|
||||
seed,
|
||||
variant,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
width: MAP_W,
|
||||
height: MAP_H,
|
||||
name: "initial-full-map",
|
||||
});
|
||||
return finalizeRectTerrainForFixedMap(seed, terrain, options);
|
||||
}
|
||||
|
||||
|
||||
export function generateTerrainAndRivers(seed, options = {}) {
|
||||
const fields = createMapFields();
|
||||
fields.visibleRavineField = new Float32Array(SIZE);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue