tweak
This commit is contained in:
parent
47af930e18
commit
e1bb10ff8a
12 changed files with 1336 additions and 526 deletions
652
mapGenerator.js
652
mapGenerator.js
|
|
@ -1,81 +1,21 @@
|
|||
import { CUSTOM_NAMES, NAME_PARTS } from "./names.js";
|
||||
import {
|
||||
applyLandscapeUnitAdminPartition,
|
||||
generateAdminRegions,
|
||||
lockSmallUrbanComponentsToMunicipality,
|
||||
mergeTinyMunicipalities,
|
||||
removeMunicipalExclaves,
|
||||
smoothAdminRegionsTerrainAware,
|
||||
snapAdminBoundariesToTerrain,
|
||||
} from "./adminRegions.js";
|
||||
import { pickEntities } from "./entitySelection.js";
|
||||
import { createMapFields } from "./fields.js";
|
||||
import { MinHeap } from "./graph.js";
|
||||
import { CELL_SIZE, INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside, nearMapEdge, xyOf } from "./grid.js";
|
||||
import { contextualDefaultName } from "./placeNameContext.js";
|
||||
import { fbm, hash2, lerp, rand, smoothstep, valueNoise } from "./random.js";
|
||||
|
||||
export const MAP_W = 172;
|
||||
export const MAP_H = 122;
|
||||
export const CELL_SIZE = 6;
|
||||
|
||||
const SIZE = MAP_W * MAP_H;
|
||||
const INF = 1e9;
|
||||
|
||||
export function indexOf(x, y) {
|
||||
return y * MAP_W + x;
|
||||
}
|
||||
|
||||
function xyOf(i) {
|
||||
return [i % MAP_W, Math.floor(i / MAP_W)];
|
||||
}
|
||||
|
||||
function inside(x, y) {
|
||||
return x >= 0 && y >= 0 && x < MAP_W && y < MAP_H;
|
||||
}
|
||||
|
||||
function clamp(v, a = 0, b = 1) {
|
||||
return Math.max(a, Math.min(b, v));
|
||||
}
|
||||
|
||||
function nearMapEdge(x, y, margin = 1) {
|
||||
return x < margin || y < margin || x >= MAP_W - margin || y >= MAP_H - margin;
|
||||
}
|
||||
|
||||
function hash2(x, y, seed) {
|
||||
let h = Math.imul((x | 0) ^ (seed | 0), 374761393) + Math.imul((y | 0) ^ ((seed >>> 1) | 0), 668265263);
|
||||
h = (h ^ (h >>> 13)) >>> 0;
|
||||
h = Math.imul(h, 1274126177) >>> 0;
|
||||
return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
|
||||
}
|
||||
|
||||
function rand(seed, n) {
|
||||
return hash2(n * 7919 + 17, n * 104729 + 31, seed);
|
||||
}
|
||||
|
||||
function smoothstep(t) {
|
||||
t = clamp(t);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
function lerp(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
function valueNoise(x, y, seed, scale) {
|
||||
const sx = x / scale;
|
||||
const sy = y / scale;
|
||||
const x0 = Math.floor(sx);
|
||||
const y0 = Math.floor(sy);
|
||||
const tx = smoothstep(sx - x0);
|
||||
const ty = smoothstep(sy - y0);
|
||||
|
||||
const a = hash2(x0, y0, seed);
|
||||
const b = hash2(x0 + 1, y0, seed);
|
||||
const c = hash2(x0, y0 + 1, seed);
|
||||
const d = hash2(x0 + 1, y0 + 1, seed);
|
||||
|
||||
return lerp(lerp(a, b, tx), lerp(c, d, tx), ty);
|
||||
}
|
||||
|
||||
function fbm(x, y, seed) {
|
||||
let amp = 1;
|
||||
let scale = 54;
|
||||
let sum = 0;
|
||||
let norm = 0;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
sum += valueNoise(x, y, seed + i * 101, scale) * amp;
|
||||
norm += amp;
|
||||
amp *= 0.5;
|
||||
scale *= 0.5;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./grid.js";
|
||||
|
||||
function neighbors8(x, y) {
|
||||
const out = [];
|
||||
|
|
@ -106,57 +46,6 @@ function distanceToNearest(points, x, y, fallback = 999) {
|
|||
return best;
|
||||
}
|
||||
|
||||
function pickEntities(candidates, { max = 99, minDistance = 5, threshold = 0, seed = 0, jitter = 0.04 } = {}) {
|
||||
const sorted = candidates
|
||||
.filter((p) => Number.isFinite(p.score) && p.score >= threshold)
|
||||
.map((p) => ({ ...p, score: p.score + hash2(p.x, p.y, seed + 54321) * jitter }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const out = [];
|
||||
for (const candidate of sorted) {
|
||||
if (out.every((p) => Math.hypot(p.x - candidate.x, p.y - candidate.y) >= minDistance)) {
|
||||
out.push(candidate);
|
||||
if (out.length >= max) break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class MinHeap {
|
||||
constructor() { this.items = []; }
|
||||
push(item) {
|
||||
this.items.push(item);
|
||||
let i = this.items.length - 1;
|
||||
while (i > 0) {
|
||||
const parent = (i - 1) >> 1;
|
||||
if (this.items[parent].f <= item.f) break;
|
||||
this.items[i] = this.items[parent];
|
||||
i = parent;
|
||||
}
|
||||
this.items[i] = item;
|
||||
}
|
||||
pop() {
|
||||
if (this.items.length === 0) return null;
|
||||
const root = this.items[0];
|
||||
const last = this.items.pop();
|
||||
if (this.items.length > 0) {
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const left = i * 2 + 1;
|
||||
const right = left + 1;
|
||||
if (left >= this.items.length) break;
|
||||
const child = right < this.items.length && this.items[right].f < this.items[left].f ? right : left;
|
||||
if (this.items[child].f >= last.f) break;
|
||||
this.items[i] = this.items[child];
|
||||
i = child;
|
||||
}
|
||||
this.items[i] = last;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
get length() { return this.items.length; }
|
||||
}
|
||||
|
||||
function aStar(start, goal, costAt) {
|
||||
const startIndex = indexOf(start.x, start.y);
|
||||
const goalIndex = indexOf(goal.x, goal.y);
|
||||
|
|
@ -673,348 +562,43 @@ function tagInsidePrefecture(points, prefectureMask) {
|
|||
return points.map((p) => ({ ...p, insidePrefecture: Boolean(prefectureMask[indexOf(p.x, p.y)]) }));
|
||||
}
|
||||
|
||||
function defaultName(seed, id) {
|
||||
const prefixKey = id.split("-")[0];
|
||||
const n = Number(id.split("-")[1] || 0);
|
||||
const prefixes = NAME_PARTS.prefixes || [""];
|
||||
const infixes = NAME_PARTS.infixes || [""];
|
||||
const suffixes = NAME_PARTS.suffixes || [""];
|
||||
const prefix = prefixes[(seed + n * 7) % prefixes.length];
|
||||
const useInfix = hash2(n + prefixKey.length * 17, seed + n * 31, seed + 2777) >= 0.7;
|
||||
const infix = useInfix ? infixes[(seed * 3 + n * 11) % infixes.length] : "";
|
||||
const suffixWord = suffixes[(seed * 5 + n * 13 + prefixKey.length) % suffixes.length];
|
||||
return `${prefix}${infix}${suffixWord}`;
|
||||
function defaultName(seed, id, entity, nameFields, attempt = 0) {
|
||||
return contextualDefaultName(seed, id, entity, NAME_PARTS, nameFields, attempt);
|
||||
}
|
||||
|
||||
function attachIdsAndNames(points, prefix, seed, kindOverride = null) {
|
||||
function attachIdsAndNames(points, prefix, seed, kindOverride = null, nameFields = null, usedNames = null) {
|
||||
return points.map((p, i) => {
|
||||
const id = `${prefix}-${i}`;
|
||||
const kind = kindOverride || p.kind;
|
||||
let name = CUSTOM_NAMES[id];
|
||||
if (!name) {
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
name = defaultName(seed + prefix.length * 1000, id, { ...p, kind }, nameFields, attempt);
|
||||
if (!usedNames || !usedNames.has(name)) break;
|
||||
}
|
||||
}
|
||||
if (usedNames) usedNames.add(name);
|
||||
return {
|
||||
...p,
|
||||
id,
|
||||
name: CUSTOM_NAMES[id] || defaultName(seed + prefix.length * 1000, id),
|
||||
name,
|
||||
insidePrefecture: Boolean(p.insidePrefecture),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse) {
|
||||
const adminId = new Int16Array(SIZE);
|
||||
adminId.fill(-1);
|
||||
const dist = new Float32Array(SIZE);
|
||||
dist.fill(INF);
|
||||
const heap = new MinHeap();
|
||||
|
||||
centers.forEach((center, regionId) => {
|
||||
const i = indexOf(center.x, center.y);
|
||||
dist[i] = 0;
|
||||
adminId[i] = regionId;
|
||||
heap.push({ i, f: 0, regionId });
|
||||
});
|
||||
|
||||
let guard = 0;
|
||||
while (heap.length > 0 && guard++ < SIZE * 12) {
|
||||
const current = heap.pop();
|
||||
if (!current) continue;
|
||||
const curIndex = current.i;
|
||||
const curRegion = adminId[curIndex];
|
||||
if (curRegion < 0) continue;
|
||||
if (current.f > dist[curIndex] + 1e-5) continue;
|
||||
|
||||
const [cx, cy] = xyOf(curIndex);
|
||||
for (const [nx, ny, step] of neighbors8(cx, cy)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
|
||||
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
|
||||
const riverBarrier = Math.max(river[ni], river[curIndex]);
|
||||
const highDivide = Math.max(elevation[ni], elevation[curIndex]);
|
||||
const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0);
|
||||
const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier;
|
||||
const slopePenalty = slope[ni] * 10.6;
|
||||
const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0;
|
||||
const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0;
|
||||
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0;
|
||||
const valleyLocalityBonus = valleyField[ni] * 0.16;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
const nextDist = dist[curIndex] + stepCost;
|
||||
|
||||
if (nextDist < dist[ni]) {
|
||||
dist[ni] = nextDist;
|
||||
adminId[ni] = curRegion;
|
||||
heap.push({ i: ni, f: nextDist, regionId: curRegion });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return adminId;
|
||||
}
|
||||
|
||||
function terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField) {
|
||||
return clamp(
|
||||
ridgeField[i] * 3.15 +
|
||||
river[i] * 2.45 +
|
||||
valleyField[i] * 0.62 +
|
||||
slope[i] * 1.06 +
|
||||
Math.max(0, elevation[i] - 0.5) * 1.18
|
||||
);
|
||||
}
|
||||
|
||||
function smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, passes = 5) {
|
||||
let current = new Int16Array(adminId);
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
const next = new Int16Array(current);
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const own = current[i];
|
||||
if (!prefectureMask[i] || sea[i] || own < 0) continue;
|
||||
const urbanCell = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || populationDensity[i] > 0.24;
|
||||
const barrier = terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField);
|
||||
if (barrier > 0.62 || urbanCell) continue;
|
||||
|
||||
const counts = new Map();
|
||||
let ownCount = 0;
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const id = current[ni];
|
||||
if (id < 0) continue;
|
||||
const weight = terrainBoundaryStrength(ni, elevation, slope, river, ridgeField, valleyField) > 0.72 ? 0.45 : 1;
|
||||
counts.set(id, (counts.get(id) || 0) + weight);
|
||||
if (id === own) ownCount += weight;
|
||||
}
|
||||
let bestId = own;
|
||||
let best = ownCount;
|
||||
for (const [id, score] of counts) {
|
||||
if (score > best) { best = score; bestId = id; }
|
||||
}
|
||||
if (bestId !== own && (best >= 4.2 || ownCount <= 2.1)) next[i] = bestId;
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
adminId.set(current);
|
||||
}
|
||||
|
||||
function lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, maxCells = 360) {
|
||||
const seen = new Uint8Array(SIZE);
|
||||
const queue = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (seen[i] || !prefectureMask[i] || sea[i]) continue;
|
||||
const isUrbanStart = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8 || populationDensity[i] > 0.20;
|
||||
if (!isUrbanStart) continue;
|
||||
const component = [];
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
seen[i] = 1;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
component.push(cur);
|
||||
const [x, y] = xyOf(cur);
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (seen[ni] || !prefectureMask[ni] || sea[ni]) continue;
|
||||
const isUrban = (landuse[ni] >= 2 && landuse[ni] <= 4) || landuse[ni] === 7 || landuse[ni] === 8 || populationDensity[ni] > 0.20;
|
||||
if (!isUrban) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
if (component.length === 0 || component.length > maxCells) continue;
|
||||
const counts = new Map();
|
||||
for (const ci of component) {
|
||||
const id = adminId[ci];
|
||||
if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1 + populationDensity[ci]);
|
||||
}
|
||||
let bestId = -1;
|
||||
let best = -1;
|
||||
for (const [id, score] of counts) {
|
||||
if (score > best) { best = score; bestId = id; }
|
||||
}
|
||||
if (bestId >= 0) for (const ci of component) adminId[ci] = bestId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities = [], minArea = 320) {
|
||||
const area = new Map();
|
||||
const pop = new Map();
|
||||
const adjacency = new Map();
|
||||
const cityMunicipalities = new Set();
|
||||
for (const city of modernCities || []) {
|
||||
if (inside(city.x, city.y)) cityMunicipalities.add(adminId[indexOf(city.x, city.y)]);
|
||||
}
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const id = adminId[i];
|
||||
if (id < 0) continue;
|
||||
area.set(id, (area.get(id) || 0) + 1);
|
||||
pop.set(id, (pop.get(id) || 0) + populationDensity[i]);
|
||||
for (const [nx, ny] of [[x+1,y],[x,y+1]]) {
|
||||
if (!inside(nx, ny)) continue;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const other = adminId[ni];
|
||||
if (other < 0 || other === id) continue;
|
||||
const key = id < other ? `${id}:${other}` : `${other}:${id}`;
|
||||
adjacency.set(key, (adjacency.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const mergeTarget = new Map();
|
||||
for (const [id, cells] of area) {
|
||||
const score = cells + (pop.get(id) || 0) * 16;
|
||||
if (cells >= minArea || cityMunicipalities.has(id)) continue;
|
||||
let bestNeighbor = -1;
|
||||
let bestScore = -1;
|
||||
for (const [key, border] of adjacency) {
|
||||
const [a, b] = key.split(':').map(Number);
|
||||
if (a !== id && b !== id) continue;
|
||||
const other = a === id ? b : a;
|
||||
const otherArea = area.get(other) || 0;
|
||||
const otherPop = pop.get(other) || 0;
|
||||
const candidate = border * 3 + otherArea * 0.012 + otherPop * 0.24;
|
||||
if (candidate > bestScore) {
|
||||
bestScore = candidate;
|
||||
bestNeighbor = other;
|
||||
}
|
||||
}
|
||||
if (bestNeighbor >= 0 && (area.get(bestNeighbor) || 0) >= score * 0.35) mergeTarget.set(id, bestNeighbor);
|
||||
}
|
||||
if (mergeTarget.size === 0) return;
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
const id = adminId[i];
|
||||
if (mergeTarget.has(id)) adminId[i] = mergeTarget.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCenters = [], protectedPoints = [], maxIslandCells = 220) {
|
||||
const protectedByAdmin = new Map();
|
||||
for (const p of [...adminCenters, ...protectedPoints]) {
|
||||
if (!p || !inside(p.x, p.y)) continue;
|
||||
const id = adminId[indexOf(p.x, p.y)];
|
||||
if (id < 0) continue;
|
||||
if (!protectedByAdmin.has(id)) protectedByAdmin.set(id, new Set());
|
||||
protectedByAdmin.get(id).add(indexOf(p.x, p.y));
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && adminId[i] >= 0) ids.add(adminId[i]);
|
||||
|
||||
const globalSeen = new Uint8Array(SIZE);
|
||||
const queue = [];
|
||||
for (const id of ids) {
|
||||
const components = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (globalSeen[i] || adminId[i] !== id || !prefectureMask[i] || sea[i]) continue;
|
||||
const comp = [];
|
||||
let hasProtected = protectedByAdmin.get(id)?.has(i) || false;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
globalSeen[i] = 1;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
comp.push(cur);
|
||||
if (protectedByAdmin.get(id)?.has(cur)) hasProtected = true;
|
||||
const [x, y] = xyOf(cur);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (globalSeen[ni] || adminId[ni] !== id || !prefectureMask[ni] || sea[ni]) continue;
|
||||
globalSeen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push({ cells: comp, hasProtected });
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) => (b.hasProtected ? 1000000 : 0) + b.cells.length - ((a.hasProtected ? 1000000 : 0) + a.cells.length));
|
||||
const keep = new Set(components[0].cells);
|
||||
for (const component of components.slice(1)) {
|
||||
const mainSize = components[0].cells.length;
|
||||
if (component.hasProtected && component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.42) continue;
|
||||
if (component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.36) continue;
|
||||
const counts = new Map();
|
||||
for (const ci of component.cells) {
|
||||
const [x, y] = xyOf(ci);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const other = adminId[ni];
|
||||
if (other >= 0 && other !== id) counts.set(other, (counts.get(other) || 0) + 1);
|
||||
}
|
||||
}
|
||||
let target = -1;
|
||||
let best = -1;
|
||||
for (const [other, count] of counts) if (count > best) { best = count; target = other; }
|
||||
if (target >= 0) for (const ci of component.cells) adminId[ci] = target;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, passes = 6) {
|
||||
let current = new Int16Array(adminId);
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
const next = new Int16Array(current);
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const own = current[i];
|
||||
if (!prefectureMask[i] || sea[i] || own < 0) continue;
|
||||
|
||||
const urbanCell = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8 || populationDensity[i] > 0.24;
|
||||
if (urbanCell) continue;
|
||||
|
||||
let isBoundary = false;
|
||||
const counts = new Map([[own, 0]]);
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const id = current[ni];
|
||||
if (id < 0) continue;
|
||||
if (id !== own) isBoundary = true;
|
||||
const terrain = terrainBoundaryStrength(ni, elevation, slope, river, ridgeField, valleyField);
|
||||
const weight = terrain > 0.60 ? 0.45 : 1.0;
|
||||
counts.set(id, (counts.get(id) || 0) + weight);
|
||||
}
|
||||
if (!isBoundary) continue;
|
||||
|
||||
let localBarrier = terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField);
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
localBarrier = Math.max(localBarrier, terrainBoundaryStrength(indexOf(nx, ny), elevation, slope, river, ridgeField, valleyField));
|
||||
}
|
||||
}
|
||||
if (localBarrier > 0.50) continue;
|
||||
|
||||
let bestId = own;
|
||||
let best = counts.get(own) || 0;
|
||||
for (const [id, score] of counts) {
|
||||
if (id === own) continue;
|
||||
const adjusted = score + (populationDensity[i] < 0.12 ? 0.42 : 0) + (plainnessForBoundary(elevation, slope, ridgeField, i) ? 0.24 : 0);
|
||||
if (adjusted > best + 0.65) {
|
||||
best = adjusted;
|
||||
bestId = id;
|
||||
}
|
||||
}
|
||||
if (bestId !== own) next[i] = bestId;
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
adminId.set(current);
|
||||
}
|
||||
|
||||
function plainnessForBoundary(elevation, slope, ridgeField, i) {
|
||||
return elevation[i] < 0.58 && slope[i] < 0.26 && ridgeField[i] < 0.34;
|
||||
function applyOutputOptions(map, options = {}) {
|
||||
if (options.includeDebugFields !== false) return map;
|
||||
const slim = { ...map };
|
||||
delete slim.settlementCluster;
|
||||
delete slim.ridgeField;
|
||||
delete slim.valleyField;
|
||||
delete slim.basinField;
|
||||
delete slim.coastalLowland;
|
||||
delete slim.flowAccum;
|
||||
delete slim.erosionField;
|
||||
delete slim.depositionField;
|
||||
return slim;
|
||||
}
|
||||
|
||||
function recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence) {
|
||||
|
|
@ -1078,32 +662,33 @@ function recalculatePopulationAfterLanduse(modernCities, satelliteCities, popula
|
|||
}
|
||||
}
|
||||
|
||||
export function generateMap(seedInput = 114514) {
|
||||
export function generateMap(seedInput = 114514, options = {}) {
|
||||
const seed = Number(seedInput) >>> 0;
|
||||
|
||||
let prefectureMask;
|
||||
let prefectureBorder;
|
||||
|
||||
const elevation = new Float32Array(SIZE);
|
||||
const moisture = new Float32Array(SIZE);
|
||||
const slope = new Float32Array(SIZE);
|
||||
const sea = new Uint8Array(SIZE);
|
||||
const river = new Float32Array(SIZE);
|
||||
const floodplain = new Float32Array(SIZE);
|
||||
const plain = new Float32Array(SIZE);
|
||||
const agriculture = new Float32Array(SIZE);
|
||||
const ridgeField = new Float32Array(SIZE);
|
||||
const valleyField = new Float32Array(SIZE);
|
||||
const basinField = new Float32Array(SIZE);
|
||||
const coastalLowland = new Float32Array(SIZE);
|
||||
const flowAccum = new Float32Array(SIZE);
|
||||
const erosionField = new Float32Array(SIZE);
|
||||
const depositionField = new Float32Array(SIZE);
|
||||
const flowTo = new Int32Array(SIZE);
|
||||
flowTo.fill(-1);
|
||||
const portSuitability = new Float32Array(SIZE);
|
||||
const crossingSuitability = new Float32Array(SIZE);
|
||||
const passSuitability = new Float32Array(SIZE);
|
||||
const {
|
||||
elevation,
|
||||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
agriculture,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
flowTo,
|
||||
portSuitability,
|
||||
crossingSuitability,
|
||||
passSuitability,
|
||||
} = createMapFields();
|
||||
|
||||
const coastAngle = rand(seed, 11) * Math.PI * 2;
|
||||
const coastX = Math.cos(coastAngle);
|
||||
|
|
@ -2000,6 +1585,20 @@ export function generateMap(seedInput = 114514) {
|
|||
predicate: (x, y, i) => !sea[i],
|
||||
}).map((p) => ({ ...p, kind: "Pass" }));
|
||||
|
||||
const settlementCluster = new Float32Array(SIZE);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const valleyCorridor = clamp(valleyField[i] * 0.62 + river[i] * 0.16);
|
||||
const lowlandCorridor = clamp(coastalLowland[i] * 0.38 + basinField[i] * 0.34 + plain[i] * 0.24 + agriculture[i] * 0.18);
|
||||
const terrainGate = clamp(1.0 - slope[i] * 1.18 - ridgeField[i] * 0.52 - Math.max(0, elevation[i] - 0.62) * 1.35, 0.08, 1);
|
||||
const localPatch = valueNoise(x * 0.7, y * 0.7, seed + 1037, 10);
|
||||
const broadPatch = fbm(x * 0.32 + 71, y * 0.32 - 19, seed + 1038);
|
||||
settlementCluster[i] = clamp((valleyCorridor + lowlandCorridor) * terrainGate * (0.72 + broadPatch * 0.42 + localPatch * 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
const settlementScore = new Float32Array(SIZE);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
|
|
@ -2009,14 +1608,16 @@ export function generateMap(seedInput = 114514) {
|
|||
for (const p of [...ports, ...crossings, ...passes]) nearFeature = Math.max(nearFeature, 1 / (1 + Math.hypot(x - p.x, y - p.y) / 4));
|
||||
const riverPull = Math.min(0.32, river[i] * 0.14 + valleyField[i] * 0.16);
|
||||
const mountainVillage = valleyField[i] * clamp(elevation[i] - 0.42, 0, 0.3) * 0.52;
|
||||
settlementScore[i] = clamp(agriculture[i] * 0.55 + plain[i] * 0.16 + nearFeature * 0.24 + riverPull + basinField[i] * 0.12 + mountainVillage - slope[i] * 0.42 - ridgeField[i] * 0.2 - floodplain[i] * 0.06);
|
||||
const remoteMountainPenalty = Math.max(0, elevation[i] - 0.58) * Math.max(0, ridgeField[i] - 0.22) * (1 - valleyField[i]) * 0.75;
|
||||
const base = agriculture[i] * 0.50 + plain[i] * 0.14 + nearFeature * 0.23 + riverPull + basinField[i] * 0.13 + coastalLowland[i] * 0.08 + mountainVillage - slope[i] * 0.48 - ridgeField[i] * 0.24 - floodplain[i] * 0.06 - remoteMountainPenalty;
|
||||
settlementScore[i] = clamp(base * (0.74 + settlementCluster[i] * 0.66) + settlementCluster[i] * 0.13);
|
||||
}
|
||||
}
|
||||
|
||||
let villages = pickPoints(settlementScore, {
|
||||
threshold: 0.32 + rand(seed, 1031) * 0.1,
|
||||
max: 28 + Math.floor(rand(seed, 1032) * 44),
|
||||
minDistance: 4 + Math.floor(rand(seed, 1033) * 4),
|
||||
minDistance: 3 + Math.floor(rand(seed, 1033) * 3),
|
||||
seedOffset: 1030,
|
||||
predicate: (x, y, i) => !sea[i],
|
||||
}).map((p) => ({ ...p, kind: "Village" }));
|
||||
|
|
@ -2481,12 +2082,35 @@ export function generateMap(seedInput = 114514) {
|
|||
|
||||
const nationalRoads = [];
|
||||
const roadDegree = new Map();
|
||||
const roadTargets = pickEntities([...modernCities.filter((p) => (p.population || 0) >= 90000), ...ports, ...markets].map((p) => ({ ...p, score: p.score + ((p.population || 0) >= 180000 ? 0.18 : 0.05) })), {
|
||||
function transportDemand(p) {
|
||||
const pop = Math.sqrt(Math.max(0, p.population || 0)) / 700;
|
||||
const capitalBoost = p.isPrefecturalCapital || p.rank === "Prefectural Capital" ? 2.1 : 0;
|
||||
const portBoost = p.portClass === "major" ? 1.4 : p.portClass === "regional" ? 0.8 : p.portClass ? 0.35 : 0;
|
||||
const historyBoost = p.kind?.includes("Castle") ? 0.55 : p.kind === "Market Town" ? 0.42 : 0;
|
||||
const gatewayBoost = p.kind === "External Gateway" ? 1.1 : 0;
|
||||
return 0.35 + pop + capitalBoost + portBoost + historyBoost + gatewayBoost;
|
||||
}
|
||||
|
||||
function sameCorridorAffinity(a, b) {
|
||||
const ai = indexOf(a.x, a.y);
|
||||
const bi = indexOf(b.x, b.y);
|
||||
return Math.min(0.6, (basinField[ai] + basinField[bi]) * 0.14 + (valleyField[ai] + valleyField[bi]) * 0.10 + (coastalLowland[ai] + coastalLowland[bi]) * 0.10);
|
||||
}
|
||||
|
||||
const roadTargetCandidates = [...modernCities.filter((p) => (p.population || 0) >= 90000), ...ports, ...markets, ...castles]
|
||||
.map((p) => ({ ...p, demand: transportDemand(p), score: (p.score || 0.4) + transportDemand(p) * 0.24 + ((p.population || 0) >= 180000 ? 0.18 : 0.05) }));
|
||||
const pickedRoadTargets = pickEntities(roadTargetCandidates, {
|
||||
max: 8 + Math.floor(rand(seed, 1101) * 10),
|
||||
minDistance: 9,
|
||||
threshold: 0,
|
||||
seed: seed + 1100,
|
||||
});
|
||||
const roadTargets = [
|
||||
capital,
|
||||
...pickedRoadTargets
|
||||
.filter((p) => Math.hypot(p.x - capital.x, p.y - capital.y) > 2)
|
||||
.sort((a, b) => transportDemand(b) - transportDemand(a)),
|
||||
];
|
||||
const roadHubs = [...modernCities, ...ports, ...markets, ...stations];
|
||||
const roadCore = [capital];
|
||||
|
||||
|
|
@ -2511,12 +2135,24 @@ export function generateMap(seedInput = 114514) {
|
|||
const anchor = nearestConnectable(roadCore, target, roadDegree, 3) || capital;
|
||||
if (addNationalRoad(anchor, target)) roadCore.push(target);
|
||||
}
|
||||
for (let i = 1; i < roadTargets.length - 1; i++) {
|
||||
const a = roadTargets[i];
|
||||
const b = pickEntities(roadTargets.filter((p) => p !== a && getDegree(roadDegree, p) < 4).map((p) => ({ ...p, score: 1 / (1 + Math.hypot(p.x - a.x, p.y - a.y)) })), { max: 1, minDistance: 1, threshold: 0 })[0];
|
||||
const d = b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
||||
if (b && d >= 18 && d < 52 && getDegree(roadDegree, a) < 4 && rand(seed, i + 1111) > 0.24) {
|
||||
addNationalRoad(a, b);
|
||||
const roadLinkCandidates = [];
|
||||
for (let i = 0; i < roadTargets.length; i++) {
|
||||
for (let j = i + 1; j < roadTargets.length; j++) {
|
||||
const a = roadTargets[i];
|
||||
const b = roadTargets[j];
|
||||
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
||||
if (d < 18 || d > 58) continue;
|
||||
const demand = Math.sqrt(transportDemand(a) * transportDemand(b));
|
||||
roadLinkCandidates.push({ a, b, score: demand / (1 + d / 18) + sameCorridorAffinity(a, b) + hash2(a.x + b.x, a.y + b.y, seed + 1111) * 0.05 });
|
||||
}
|
||||
}
|
||||
roadLinkCandidates.sort((a, b) => b.score - a.score);
|
||||
let extraRoadLinks = 0;
|
||||
for (const link of roadLinkCandidates) {
|
||||
if (extraRoadLinks >= 4) break;
|
||||
if (getDegree(roadDegree, link.a) >= 4 || getDegree(roadDegree, link.b) >= 4) continue;
|
||||
if (addNationalRoad(link.a, link.b)) {
|
||||
extraRoadLinks++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3229,7 +2865,8 @@ export function generateMap(seedInput = 114514) {
|
|||
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 620);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 260);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 180);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, 5);
|
||||
applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 5);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 360);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 220);
|
||||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
|
@ -3276,24 +2913,26 @@ export function generateMap(seedInput = 114514) {
|
|||
const abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
|
||||
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
|
||||
const preservedOldRoads = premodernRoads.filter((_, i) => i % 2 === 0);
|
||||
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland };
|
||||
const usedNames = new Set(Object.values(CUSTOM_NAMES));
|
||||
|
||||
villages = attachIdsAndNames(tagInsidePrefecture(villages, prefectureMask), "village", seed);
|
||||
ports = attachIdsAndNames(tagInsidePrefecture(ports, prefectureMask), "port", seed);
|
||||
crossings = attachIdsAndNames(tagInsidePrefecture(crossings, prefectureMask), "crossing", seed);
|
||||
passes = attachIdsAndNames(tagInsidePrefecture(passes, prefectureMask), "pass", seed);
|
||||
markets = attachIdsAndNames(tagInsidePrefecture(markets, prefectureMask), "market", seed);
|
||||
castles = attachIdsAndNames(tagInsidePrefecture(castles, prefectureMask), "castle", seed);
|
||||
castleTowns = attachIdsAndNames(tagInsidePrefecture(castleTowns, prefectureMask), "castleTown", seed);
|
||||
modernCities = attachIdsAndNames(tagInsidePrefecture(modernCities, prefectureMask), "city", seed);
|
||||
stations = attachIdsAndNames(tagInsidePrefecture(stations, prefectureMask), "station", seed);
|
||||
industrialZones = attachIdsAndNames(tagInsidePrefecture(industrialZones, prefectureMask), "industrial", seed);
|
||||
interchanges = attachIdsAndNames(tagInsidePrefecture(interchanges, prefectureMask), "interchange", seed);
|
||||
logisticsParks = attachIdsAndNames(tagInsidePrefecture(logisticsParks, prefectureMask), "logistics", seed);
|
||||
satelliteCities = attachIdsAndNames(tagInsidePrefecture(satelliteCities, prefectureMask), "satellite", seed);
|
||||
newTowns = attachIdsAndNames(tagInsidePrefecture(newTowns, prefectureMask), "newtown", seed);
|
||||
castleRuins = attachIdsAndNames(tagInsidePrefecture(castleRuins, prefectureMask), "castleRuin", seed);
|
||||
externalGateways = attachIdsAndNames(tagInsidePrefecture(externalGateways, prefectureMask), "gateway", seed, "External Gateway");
|
||||
const adminCenters = attachIdsAndNames(tagInsidePrefecture(adminCentersRaw, prefectureMask), "admin", seed, "Municipal Center");
|
||||
villages = attachIdsAndNames(tagInsidePrefecture(villages, prefectureMask), "village", seed, null, nameFields, usedNames);
|
||||
ports = attachIdsAndNames(tagInsidePrefecture(ports, prefectureMask), "port", seed, null, nameFields, usedNames);
|
||||
crossings = attachIdsAndNames(tagInsidePrefecture(crossings, prefectureMask), "crossing", seed, null, nameFields, usedNames);
|
||||
passes = attachIdsAndNames(tagInsidePrefecture(passes, prefectureMask), "pass", seed, null, nameFields, usedNames);
|
||||
markets = attachIdsAndNames(tagInsidePrefecture(markets, prefectureMask), "market", seed, null, nameFields, usedNames);
|
||||
castles = attachIdsAndNames(tagInsidePrefecture(castles, prefectureMask), "castle", seed, null, nameFields, usedNames);
|
||||
castleTowns = attachIdsAndNames(tagInsidePrefecture(castleTowns, prefectureMask), "castleTown", seed, null, nameFields, usedNames);
|
||||
modernCities = attachIdsAndNames(tagInsidePrefecture(modernCities, prefectureMask), "city", seed, null, nameFields, usedNames);
|
||||
stations = attachIdsAndNames(tagInsidePrefecture(stations, prefectureMask), "station", seed, null, nameFields, usedNames);
|
||||
industrialZones = attachIdsAndNames(tagInsidePrefecture(industrialZones, prefectureMask), "industrial", seed, null, nameFields, usedNames);
|
||||
interchanges = attachIdsAndNames(tagInsidePrefecture(interchanges, prefectureMask), "interchange", seed, null, nameFields, usedNames);
|
||||
logisticsParks = attachIdsAndNames(tagInsidePrefecture(logisticsParks, prefectureMask), "logistics", seed, null, nameFields, usedNames);
|
||||
satelliteCities = attachIdsAndNames(tagInsidePrefecture(satelliteCities, prefectureMask), "satellite", seed, null, nameFields, usedNames);
|
||||
newTowns = attachIdsAndNames(tagInsidePrefecture(newTowns, prefectureMask), "newtown", seed, null, nameFields, usedNames);
|
||||
castleRuins = attachIdsAndNames(tagInsidePrefecture(castleRuins, prefectureMask), "castleRuin", seed, null, nameFields, usedNames);
|
||||
externalGateways = attachIdsAndNames(tagInsidePrefecture(externalGateways, prefectureMask), "gateway", seed, "External Gateway", nameFields, usedNames);
|
||||
const adminCenters = attachIdsAndNames(tagInsidePrefecture(adminCentersRaw, prefectureMask), "admin", seed, "Municipal Center", nameFields, usedNames);
|
||||
|
||||
const entitiesForNames = [
|
||||
...modernCities,
|
||||
|
|
@ -3311,7 +2950,7 @@ export function generateMap(seedInput = 114514) {
|
|||
...externalGateways,
|
||||
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
|
||||
|
||||
return {
|
||||
return applyOutputOptions({
|
||||
width: MAP_W,
|
||||
height: MAP_H,
|
||||
cellSize: CELL_SIZE,
|
||||
|
|
@ -3327,6 +2966,7 @@ export function generateMap(seedInput = 114514) {
|
|||
floodplain,
|
||||
plain,
|
||||
agriculture,
|
||||
settlementCluster,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
basinField,
|
||||
|
|
@ -3380,5 +3020,5 @@ export function generateMap(seedInput = 114514) {
|
|||
smallStreams,
|
||||
externalGateways,
|
||||
entitiesForNames,
|
||||
};
|
||||
}, options);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue