tweeaaaaaaaaak
This commit is contained in:
parent
32398c13e8
commit
47af930e18
8 changed files with 446 additions and 136 deletions
46
README.md
46
README.md
|
|
@ -1,46 +0,0 @@
|
|||
# Prefecture Map Generator v16 - Causal Terrain Cleanup Edition
|
||||
|
||||
This version is designed to run directly in VSCode with **Live Server**. No npm setup is required.
|
||||
|
||||
## Run
|
||||
|
||||
1. Open this folder in VSCode.
|
||||
2. Right-click `index.html`.
|
||||
3. Choose **Open with Live Server**.
|
||||
|
||||
## Main changes in v16
|
||||
|
||||
- River action is much stronger. `erosionField` and `depositionField` now produce clearer valleys, terraces, and alluvial lowlands.
|
||||
- Neighboring prefecture regions are generated as background territory with `prefectureRegionId` and `regionalPrefectureBorders`, so the current prefecture is no longer visually isolated.
|
||||
- River color now uses the same blue family as the sea; hierarchy is expressed by width and opacity.
|
||||
- CBD/DID cells are sparser and restricted to dense, connected urban fabric.
|
||||
- Railways, roads, and expressways are pruned for long-distance purpose; compact local tangles are rejected except for environmental ring segments.
|
||||
- Expressway rings are rare. ICs are sampled at shorter intervals along expressway corridors.
|
||||
- Bridge and tunnel icon rendering was removed. The legacy arrays remain empty for compatibility.
|
||||
- Municipal regions are less likely to split small urban areas, while large cities claim larger municipality-like areas.
|
||||
|
||||
## Existing systems retained
|
||||
|
||||
- River hierarchy: main rivers, tributaries, and small streams.
|
||||
- Port classes: major, regional, fishing, lake.
|
||||
- Major rail corridors first, then purposeful branches.
|
||||
- Expressways avoid city centers; city access is represented through IC access roads.
|
||||
- Castles are restricted to terrain/historical candidate sites.
|
||||
- Default generated names and manual overrides are both managed in `names.js`.
|
||||
- Continuous terrain rendering is always on.
|
||||
|
||||
## Editing place names
|
||||
|
||||
Edit `names.js`.
|
||||
|
||||
```js
|
||||
export const CUSTOM_NAMES = {
|
||||
"city-0": "Aohara",
|
||||
"port-0": "Shirahama",
|
||||
"castle-0": "Kurono"
|
||||
};
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Open `test.html` with Live Server to run the browser-side smoke tests, or run `node --check *.js` on the source files.
|
||||
2
app.js
2
app.js
|
|
@ -70,7 +70,7 @@ function getStats(map) {
|
|||
["Rail Lines", map.railways.length + map.branchRailways.length + (map.ringRailways || []).length + map.externalRailways.length],
|
||||
["Industrial Zones", countText(map.industrialZones)],
|
||||
["National Roads", map.nationalRoads.length + (map.ringRoads || []).length],
|
||||
["Expressways", map.expressways.length + (map.ringExpressways || []).length + map.externalExpressways.length],
|
||||
["Expressways", map.expressways.length + map.externalExpressways.length],
|
||||
["External Gateways", map.externalGateways.length],
|
||||
["Interchanges", countText(map.interchanges)],
|
||||
["Logistics Parks", countText(map.logisticsParks)],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Prefecture Map Generator v16</title>
|
||||
<title>Prefecture Map Generator v17</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
<section class="main-panel">
|
||||
<header class="header">
|
||||
<div>
|
||||
<h1>Prefecture Map Generator v16</h1>
|
||||
<h1>Prefecture Map Generator v17</h1>
|
||||
<p>
|
||||
Terrain-highlighted prefecture generation with terrain-snapped municipalities, a clear prefectural capital,
|
||||
hidden small streams, city-seeking national roads, and hover tooltips.
|
||||
|
|
|
|||
484
mapGenerator.js
484
mapGenerator.js
|
|
@ -1,4 +1,4 @@
|
|||
import { CUSTOM_NAMES, KIND_SUFFIXES, NAME_PARTS } from "./names.js";
|
||||
import { CUSTOM_NAMES, NAME_PARTS } from "./names.js";
|
||||
|
||||
export const MAP_W = 172;
|
||||
export const MAP_H = 122;
|
||||
|
|
@ -90,6 +90,16 @@ function neighbors8(x, y) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function neighbors4(x, y) {
|
||||
const out = [];
|
||||
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (inside(nx, ny)) out.push([nx, ny, 1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function distanceToNearest(points, x, y, fallback = 999) {
|
||||
let best = fallback;
|
||||
for (const p of points) best = Math.min(best, Math.hypot(p.x - x, p.y - y));
|
||||
|
|
@ -663,7 +673,7 @@ function tagInsidePrefecture(points, prefectureMask) {
|
|||
return points.map((p) => ({ ...p, insidePrefecture: Boolean(prefectureMask[indexOf(p.x, p.y)]) }));
|
||||
}
|
||||
|
||||
function defaultName(seed, id, kind) {
|
||||
function defaultName(seed, id) {
|
||||
const prefixKey = id.split("-")[0];
|
||||
const n = Number(id.split("-")[1] || 0);
|
||||
const prefixes = NAME_PARTS.prefixes || [""];
|
||||
|
|
@ -673,8 +683,7 @@ function defaultName(seed, id, kind) {
|
|||
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];
|
||||
const kindSuffix = KIND_SUFFIXES[prefixKey] || kind || "";
|
||||
return `${prefix}${infix}${suffixWord}${kindSuffix}`;
|
||||
return `${prefix}${infix}${suffixWord}`;
|
||||
}
|
||||
|
||||
function attachIdsAndNames(points, prefix, seed, kindOverride = null) {
|
||||
|
|
@ -684,7 +693,7 @@ function attachIdsAndNames(points, prefix, seed, kindOverride = null) {
|
|||
return {
|
||||
...p,
|
||||
id,
|
||||
name: CUSTOM_NAMES[id] || defaultName(seed + prefix.length * 1000, id, kind),
|
||||
name: CUSTOM_NAMES[id] || defaultName(seed + prefix.length * 1000, id),
|
||||
insidePrefecture: Boolean(p.insidePrefecture),
|
||||
};
|
||||
});
|
||||
|
|
@ -720,12 +729,15 @@ function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, ri
|
|||
|
||||
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
|
||||
const riverBarrier = Math.max(river[ni], river[curIndex]);
|
||||
const ridgePenalty = Math.max(0, elevation[ni] - 0.40) * 9.4 + Math.abs(elevation[ni] - elevation[curIndex]) * 8.8 + ridgeBarrier * 14.2;
|
||||
const slopePenalty = slope[ni] * 7.8;
|
||||
const riverPenalty = riverBarrier > 0.7 ? 13.0 : riverBarrier > 0.42 ? 8.2 : riverBarrier > 0.22 ? 3.8 : 0;
|
||||
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.95 : 0;
|
||||
const valleyLocalityBonus = valleyField[ni] * 0.03;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
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]) {
|
||||
|
|
@ -741,11 +753,11 @@ function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, ri
|
|||
|
||||
function terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField) {
|
||||
return clamp(
|
||||
ridgeField[i] * 2.75 +
|
||||
river[i] * 2.05 +
|
||||
slope[i] * 1.18 +
|
||||
Math.max(0, elevation[i] - 0.5) * 1.05 -
|
||||
valleyField[i] * 0.10
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -764,7 +776,7 @@ function smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation,
|
|||
|
||||
const counts = new Map();
|
||||
let ownCount = 0;
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const id = current[ni];
|
||||
|
|
@ -880,6 +892,192 @@ function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
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 recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence) {
|
||||
populationDensity.fill(0);
|
||||
const allCities = [...modernCities, ...satelliteCities];
|
||||
for (const city of allCities) {
|
||||
const urbanR = Math.max(4, city.urbanRadius || 8);
|
||||
const coreR = Math.max(2, city.coreRadius || 3);
|
||||
const popScale = clamp((Math.log10(Math.max(12000, city.population || 12000)) - 4) / 2.25, 0.16, 1.65);
|
||||
const r = Math.ceil(urbanR * 2.2);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const x = city.x + dx;
|
||||
const y = city.y + dy;
|
||||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || !prefectureMask[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const lu = landuse[i];
|
||||
const landuseWeight = lu === 3 ? 1.85 : lu === 2 ? 1.42 : lu === 4 ? 1.05 : lu === 7 ? 0.82 : lu === 8 ? 0.68 : 0.10;
|
||||
const radial = 1 / (1 + Math.pow(d / urbanR, 2.5));
|
||||
const core = Math.exp(-(d * d) / (coreR * coreR * 2.0));
|
||||
const transit = Math.max(stationInfluence?.[i] || 0, (railInfluence?.[i] || 0) * 0.55, (roadInfluence?.[i] || 0) * 0.24);
|
||||
populationDensity[i] += popScale * landuseWeight * (radial * 0.78 + core * 0.38 + transit * 0.18);
|
||||
}
|
||||
}
|
||||
}
|
||||
let maxDensity = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) maxDensity = Math.max(maxDensity, populationDensity[i]);
|
||||
if (maxDensity > 0) for (let i = 0; i < SIZE; i++) populationDensity[i] = clamp(populationDensity[i] / maxDensity);
|
||||
|
||||
for (const city of allCities) {
|
||||
let urbanCells = 0;
|
||||
let coreCells = 0;
|
||||
let densitySum = 0;
|
||||
const r = Math.ceil((city.urbanRadius || 8) * 2.0);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
const x = city.x + dx;
|
||||
const y = city.y + dy;
|
||||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > r) continue;
|
||||
const lu = landuse[i];
|
||||
if (lu >= 2 && lu <= 8) {
|
||||
urbanCells++;
|
||||
densitySum += populationDensity[i];
|
||||
if (lu === 3) coreCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const base = city.isPrefecturalCapital ? 90000 : city.kind === "Satellite City" ? 16000 : 32000;
|
||||
const urbanComponent = urbanCells * (city.isPrefecturalCapital ? 1500 : city.kind === "Satellite City" ? 900 : 1200);
|
||||
const coreComponent = coreCells * 3200;
|
||||
const densityComponent = densitySum * 650;
|
||||
city.population = Math.round((base + urbanComponent + coreComponent + densityComponent) / 1000) * 1000;
|
||||
city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 95, city.kind === "Satellite City" ? 5 : 7, city.isPrefecturalCapital ? 34 : 28);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 360, 2.2, 9);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMap(seedInput = 114514) {
|
||||
const seed = Number(seedInput) >>> 0;
|
||||
|
||||
|
|
@ -1495,8 +1693,7 @@ export function generateMap(seedInput = 114514) {
|
|||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius || d === 0) continue;
|
||||
const weight = (radius + 0.35 - d) / (radius + 0.35);
|
||||
const highRidgeGuard = clamp((elevation[ni] - 0.68) / 0.18) * clamp(ridgeField[ni] * 1.2);
|
||||
const carve = Math.max(0, weight) * (0.008 + r * 0.026) * Math.max(0.45, slope[ni] + 0.22) * (1 - highRidgeGuard * 0.72);
|
||||
const carve = Math.max(0, weight) * (0.008 + r * 0.026) * 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);
|
||||
|
|
@ -1523,6 +1720,57 @@ export function generateMap(seedInput = 114514) {
|
|||
}
|
||||
|
||||
elevation.set(fluvialElevation);
|
||||
|
||||
// Broad alluvial/coastal/basin plains. The plain score alone is not enough;
|
||||
// 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++) {
|
||||
const nextElevation = new Float32Array(elevation);
|
||||
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 lowland = clamp(
|
||||
coastalLowland[i] * 0.72 +
|
||||
basinField[i] * 0.54 +
|
||||
valleyField[i] * 0.34 +
|
||||
Math.pow(flowAccum[i], 0.58) * 0.24 -
|
||||
ridgeField[i] * 0.62 -
|
||||
Math.max(0, elevation[i] - 0.54) * 1.65 -
|
||||
slope[i] * 0.74
|
||||
);
|
||||
if (lowland <= 0.12) continue;
|
||||
let sum = 0;
|
||||
let weight = 0;
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 2.25) continue;
|
||||
const compatible = clamp(1 - Math.abs(elevation[ni] - elevation[i]) / 0.11);
|
||||
const w = compatible / (1 + d);
|
||||
sum += elevation[ni] * w;
|
||||
weight += w;
|
||||
}
|
||||
}
|
||||
if (weight <= 0) continue;
|
||||
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);
|
||||
if (lowland > 0.55) {
|
||||
depositionField[i] = clamp(depositionField[i] + lowland * 0.018);
|
||||
erosionField[i] = Math.max(0, erosionField[i] - lowland * 0.012);
|
||||
}
|
||||
}
|
||||
}
|
||||
elevation.set(nextElevation);
|
||||
}
|
||||
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
|
|
@ -1871,12 +2119,47 @@ export function generateMap(seedInput = 114514) {
|
|||
for (const p of near) addPremodernRoad(market, p);
|
||||
}
|
||||
|
||||
function urbanSiteSuitability(p) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
if (sea[i]) return 0;
|
||||
const portBonus = p.kind === "Port Town" || p.portClass === "major" || p.portClass === "regional" ? 0.18 : 0;
|
||||
const historicalBonus = p.kind === "Market City" || p.kind === "Castle Town" ? 0.05 : 0;
|
||||
return clamp(
|
||||
plain[i] * 0.46 +
|
||||
agriculture[i] * 0.18 +
|
||||
basinField[i] * 0.20 +
|
||||
coastalLowland[i] * 0.20 +
|
||||
valleyField[i] * 0.12 +
|
||||
portBonus + historicalBonus -
|
||||
slope[i] * 0.58 -
|
||||
ridgeField[i] * 0.34 -
|
||||
Math.max(0, elevation[i] - 0.55) * 1.35
|
||||
);
|
||||
}
|
||||
|
||||
function cityPopulationCap(p) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const suitability = urbanSiteSuitability(p);
|
||||
if (suitability < 0.18 || elevation[i] > 0.66 || slope[i] > 0.82 || ridgeField[i] > 0.72) return 85000;
|
||||
if (suitability < 0.28 || elevation[i] > 0.60 || slope[i] > 0.62) return 180000;
|
||||
if (suitability < 0.38) return 420000;
|
||||
return INF;
|
||||
}
|
||||
|
||||
let castleTowns = castles.map((c) => ({ x: c.x, y: c.y, score: c.score + 0.45, kind: "Castle Town" }));
|
||||
const cityCandidates = [
|
||||
...castleTowns.map((p) => ({ ...p, score: p.score + 0.4 })),
|
||||
...ports.map((p) => ({ ...p, kind: "Port Town", score: p.score + 0.28 })),
|
||||
...markets.map((p) => ({ ...p, kind: "Market City", score: p.score + 0.12 })),
|
||||
];
|
||||
].map((p) => {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const suitability = urbanSiteSuitability(p);
|
||||
return {
|
||||
...p,
|
||||
urbanSuitability: suitability,
|
||||
score: p.score + suitability * 0.72 - slope[i] * 0.20 - ridgeField[i] * 0.16 - Math.max(0, elevation[i] - 0.58) * 0.78,
|
||||
};
|
||||
}).filter((p) => p.urbanSuitability >= 0.10 || p.kind === "Castle Town");
|
||||
|
||||
let modernCities = pickEntities(cityCandidates, {
|
||||
max: 7 + Math.floor(rand(seed, 1061) * 10),
|
||||
|
|
@ -1890,30 +2173,78 @@ export function generateMap(seedInput = 114514) {
|
|||
const rankBase = rank === "Prefectural Capital" ? 420000 : rank === "Regional Center" ? 115000 : 26000;
|
||||
const rankSpread = rank === "Prefectural Capital" ? 1450000 : rank === "Regional Center" ? 520000 : 185000;
|
||||
const pi = indexOf(p.x, p.y);
|
||||
const geographyBoost = clamp(plain[pi] * 0.34 + agriculture[pi] * 0.18 + basinField[pi] * 0.2 + coastalLowland[pi] * 0.18 + valleyField[pi] * 0.12 + (p.kind === "Port Town" ? 0.22 : 0));
|
||||
const population = Math.round((rankBase + rankSpread * Math.pow(rawScale + geographyBoost * 0.18, 1.75)) / 1000) * 1000;
|
||||
const suitability = p.urbanSuitability ?? urbanSiteSuitability(p);
|
||||
const geographyBoost = clamp(plain[pi] * 0.34 + agriculture[pi] * 0.18 + basinField[pi] * 0.2 + coastalLowland[pi] * 0.18 + valleyField[pi] * 0.12 + suitability * 0.24 + (p.kind === "Port Town" ? 0.22 : 0));
|
||||
const rawPopulation = Math.round((rankBase + rankSpread * Math.pow(rawScale + geographyBoost * 0.18, 1.75)) / 1000) * 1000;
|
||||
const population = Math.min(rawPopulation, cityPopulationCap(p));
|
||||
const urbanRadius = clamp(7.5 + Math.sqrt(population) / 80 + (rank === "Prefectural Capital" ? 3.0 : rank === "Regional Center" ? 1.5 : 0), 8, 32);
|
||||
const coreRadius = clamp(2.6 + Math.sqrt(population) / 320, 3, 9);
|
||||
const urbanWeight = clamp(0.74 + Math.log10(Math.max(10000, population)) * 0.36, 1.15, 3.05);
|
||||
return { ...p, population, urbanRadius, coreRadius, urbanWeight, rank, kind: p.kind || "City" };
|
||||
});
|
||||
|
||||
function fallbackCapitalCandidate() {
|
||||
const pools = [...markets, ...ports, ...villages].filter((p) => p && prefectureMask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)]);
|
||||
let best = null;
|
||||
let bestScore = -INF;
|
||||
for (const p of pools) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const score = urbanSiteSuitability(p) * 1.6 + plain[i] * 0.32 + populationDensityProxyForCapital(i) + (p.kind?.includes("Port") ? 0.18 : 0) + (p.score || 0);
|
||||
if (score > bestScore) { bestScore = score; best = p; }
|
||||
}
|
||||
if (best) return { ...best, kind: "Market City", population: 360000, urbanRadius: 15, coreRadius: 4.6, urbanWeight: 1.9, score: bestScore };
|
||||
|
||||
for (let y = 4; y < MAP_H - 4; y++) {
|
||||
for (let x = 4; x < MAP_W - 4; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const score = plain[i] * 0.72 + agriculture[i] * 0.24 + basinField[i] * 0.18 + coastalLowland[i] * 0.14 - slope[i] * 0.72 - ridgeField[i] * 0.32;
|
||||
if (score > bestScore) { bestScore = score; best = { x, y, score, kind: "Market City" }; }
|
||||
}
|
||||
}
|
||||
return best ? { ...best, population: 320000, urbanRadius: 14, coreRadius: 4.2, urbanWeight: 1.7 } : null;
|
||||
}
|
||||
|
||||
function populationDensityProxyForCapital(i) {
|
||||
return settlementScore[i] * 0.18 + marketScore[i] * 0.12;
|
||||
}
|
||||
|
||||
if (modernCities.length === 0 || !modernCities.some((city) => prefectureMask[indexOf(city.x, city.y)])) {
|
||||
const fallbackCapital = fallbackCapitalCandidate();
|
||||
if (fallbackCapital) modernCities.unshift(fallbackCapital);
|
||||
}
|
||||
|
||||
if (modernCities.length > 0) {
|
||||
modernCities.sort((a, b) => (b.population || 0) + b.score * 90000 - ((a.population || 0) + a.score * 90000));
|
||||
let capitalIndex = -1;
|
||||
let capitalScore = -INF;
|
||||
for (let i = 0; i < modernCities.length; i++) {
|
||||
const city = modernCities[i];
|
||||
const ci = indexOf(city.x, city.y);
|
||||
if (!prefectureMask[ci] || sea[ci]) continue;
|
||||
const suitability = urbanSiteSuitability(city);
|
||||
const score = suitability * 900000 + (city.population || 0) * 0.55 + (city.score || 0) * 120000 - slope[ci] * 180000 - Math.max(0, elevation[ci] - 0.58) * 360000;
|
||||
if (score > capitalScore) { capitalScore = score; capitalIndex = i; }
|
||||
}
|
||||
if (capitalIndex > 0) modernCities.unshift(modernCities.splice(capitalIndex, 1)[0]);
|
||||
const capCell = indexOf(modernCities[0].x, modernCities[0].y);
|
||||
const capPopulation = prefectureMask[capCell]
|
||||
? Math.max(modernCities[0].population || 0, 620000)
|
||||
: Math.min(modernCities[0].population || 0, 180000);
|
||||
modernCities[0] = {
|
||||
...modernCities[0],
|
||||
rank: "Prefectural Capital",
|
||||
kind: "Prefectural Capital",
|
||||
isPrefecturalCapital: true,
|
||||
population: Math.max(modernCities[0].population || 0, 620000),
|
||||
urbanRadius: Math.max(modernCities[0].urbanRadius || 0, 18),
|
||||
coreRadius: Math.max(modernCities[0].coreRadius || 0, 5.5),
|
||||
urbanWeight: Math.max(modernCities[0].urbanWeight || 0, 2.15),
|
||||
rank: prefectureMask[capCell] ? "Prefectural Capital" : "Regional Center",
|
||||
kind: prefectureMask[capCell] ? "Prefectural Capital" : (modernCities[0].kind || "City"),
|
||||
isPrefecturalCapital: Boolean(prefectureMask[capCell]),
|
||||
population: capPopulation,
|
||||
urbanRadius: prefectureMask[capCell] ? Math.max(modernCities[0].urbanRadius || 0, 18) : modernCities[0].urbanRadius,
|
||||
coreRadius: prefectureMask[capCell] ? Math.max(modernCities[0].coreRadius || 0, 5.5) : modernCities[0].coreRadius,
|
||||
urbanWeight: prefectureMask[capCell] ? Math.max(modernCities[0].urbanWeight || 0, 2.15) : modernCities[0].urbanWeight,
|
||||
};
|
||||
for (let i = 1; i < modernCities.length; i++) modernCities[i] = { ...modernCities[i], isPrefecturalCapital: false };
|
||||
}
|
||||
|
||||
const capital = modernCities[0] || markets[0] || ports[0] || { x: Math.floor(MAP_W / 2), y: Math.floor(MAP_H / 2), score: 1, population: 0, urbanRadius: 12, coreRadius: 4, urbanWeight: 1, isPrefecturalCapital: true };
|
||||
const capital = modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]) || modernCities.find((city) => prefectureMask[indexOf(city.x, city.y)]) || markets.find((p) => prefectureMask[indexOf(p.x, p.y)]) || ports.find((p) => prefectureMask[indexOf(p.x, p.y)]) || { x: Math.floor(MAP_W / 2), y: Math.floor(MAP_H / 2), score: 1, population: 0, urbanRadius: 12, coreRadius: 4, urbanWeight: 1, isPrefecturalCapital: true };
|
||||
|
||||
const populationDensity = new Float32Array(SIZE);
|
||||
let maxPopulationDensity = 0;
|
||||
|
|
@ -1964,19 +2295,19 @@ export function generateMap(seedInput = 114514) {
|
|||
const i = indexOf(x, y);
|
||||
const e = elevation[i];
|
||||
const s = slope[i];
|
||||
const pass = nearPassPoint(x, y, type === "express" ? 7 : 5);
|
||||
if (e > 0.86) return INF;
|
||||
if (pass && e > 0.82 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.80) return INF;
|
||||
if (!pass && e > 0.72 && s > 0.18) return INF;
|
||||
if (!pass && e > 0.68 && s > 0.32) return INF;
|
||||
if (!pass && e > 0.74) return type === "express" ? 175 : 215;
|
||||
if (!pass && e > 0.66 && s > 0.22) return type === "express" ? 105 : 135;
|
||||
const passDiscount = pass ? 0.18 : 1;
|
||||
const mountain = Math.max(0, e - 0.50);
|
||||
const steep = Math.max(0, s - 0.18);
|
||||
const typeFactor = type === "express" ? 260 : type === "rail" ? 320 : 120;
|
||||
return (mountain * mountain * typeFactor + steep * steep * 120) * passDiscount;
|
||||
const pass = nearPassPoint(x, y, type === "express" ? 7 : type === "rail" ? 6 : 5);
|
||||
if (e > 0.84) return INF;
|
||||
if (pass && e > 0.80 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.78) return INF;
|
||||
if (!pass && e > 0.70 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.66 && s > 0.28) return INF;
|
||||
if (!pass && e > 0.72) return type === "express" ? 260 : type === "rail" ? 330 : type === "minor" ? 80 : 155;
|
||||
if (!pass && e > 0.64 && s > 0.20) return type === "express" ? 145 : type === "rail" ? 180 : type === "minor" ? 54 : 96;
|
||||
const passDiscount = pass ? (type === "minor" ? 0.35 : 0.22) : 1;
|
||||
const mountain = Math.max(0, e - 0.48);
|
||||
const steep = Math.max(0, s - 0.15);
|
||||
const typeFactor = type === "express" ? 360 : type === "rail" ? 430 : type === "minor" ? 115 : 210;
|
||||
return (mountain * mountain * typeFactor + steep * steep * 150 + ridgeField[i] * 9.5) * passDiscount;
|
||||
}
|
||||
|
||||
function transportAccessPoint(node, mode = "road", salt = 0) {
|
||||
|
|
@ -1994,7 +2325,7 @@ export function generateMap(seedInput = 114514) {
|
|||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const barrier = mode === "express" || mode === "rail" ? mountainBarrierPenalty(x, y, mode) : 0;
|
||||
const barrier = mode === "express" || mode === "rail" ? mountainBarrierPenalty(x, y, mode) : mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) continue;
|
||||
const targetD = (minR + maxR) * 0.5;
|
||||
const flatness = plain[i] * 1.0 + agriculture[i] * 0.2 + valleyField[i] * 0.26 + coastalLowland[i] * 0.16 - slope[i] * 1.22 - ridgeField[i] * 0.72 - Math.max(0, elevation[i] - 0.58) * 2.35;
|
||||
|
|
@ -2127,9 +2458,11 @@ export function generateMap(seedInput = 114514) {
|
|||
function roadCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) return INF;
|
||||
const density = densityValue(x, y);
|
||||
const nodeAvoid = distanceToNearest(townAvoidNodes, x, y) < 2.2 ? 2.0 : 0;
|
||||
return Math.max(0.35, 1 + slope[i] * 15.8 + Math.max(0, elevation[i] - 0.56) * 2.6 + nodeAvoid + (river[i] > 0.45 ? 0.85 : 0) + floodplain[i] * 0.22 - density * 0.50 - plain[i] * 0.18 - valleyField[i] * 0.24 - coastalLowland[i] * 0.18 + ridgeField[i] * 0.90 + normalEdgePenalty(x, y) + hash2(x, y, seed + 333) * 0.08);
|
||||
return Math.max(0.35, 1 + slope[i] * 17.8 + barrier + Math.max(0, elevation[i] - 0.54) * 9.2 + nodeAvoid + (river[i] > 0.45 ? 0.85 : 0) + floodplain[i] * 0.22 - density * 0.50 - plain[i] * 0.22 - valleyField[i] * 0.28 - coastalLowland[i] * 0.20 + ridgeField[i] * 1.15 + normalEdgePenalty(x, y) + hash2(x, y, seed + 333) * 0.08);
|
||||
}
|
||||
|
||||
function expresswayCost(x, y) {
|
||||
|
|
@ -2255,7 +2588,7 @@ export function generateMap(seedInput = 114514) {
|
|||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || !prefectureMask[i]) continue;
|
||||
const barrier = mode === "road" ? 0 : mountainBarrierPenalty(x, y, mode === "express" ? "express" : "rail");
|
||||
const barrier = mode === "road" ? mountainBarrierPenalty(x, y, "road") : mountainBarrierPenalty(x, y, mode === "express" ? "express" : "rail");
|
||||
if (barrier >= INF) continue;
|
||||
const density = densityValue(x, y);
|
||||
const densityTerm = mode === "rail" ? density * 0.75 : mode === "express" ? midDensityAffinity(x, y) * 0.72 : density * 0.28 + midDensityAffinity(x, y) * 0.22;
|
||||
|
|
@ -2287,15 +2620,17 @@ export function generateMap(seedInput = 114514) {
|
|||
|
||||
function softRingRailCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || elevation[i] > 0.82) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "rail");
|
||||
if (sea[i] || barrier >= INF) return INF;
|
||||
const density = densityValue(x, y);
|
||||
return Math.max(0.38, 1 + slope[i] * 12 + Math.max(0, elevation[i] - 0.58) * 16 + (river[i] > 0.5 ? 1.3 : river[i] * 0.6) - density * 0.62 - plain[i] * 0.18 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7222) * 0.05);
|
||||
return Math.max(0.38, 1 + slope[i] * 14 + barrier + Math.max(0, elevation[i] - 0.56) * 22 + (river[i] > 0.5 ? 1.3 : river[i] * 0.6) - density * 0.62 - plain[i] * 0.20 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7222) * 0.05);
|
||||
}
|
||||
|
||||
function softRingExpressCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || elevation[i] > 0.82) return INF;
|
||||
return Math.max(0.38, 1 + slope[i] * 11 + Math.max(0, elevation[i] - 0.6) * 14 + (river[i] > 0.5 ? 1.0 : river[i] * 0.5) - midDensityAffinity(x, y) * 0.42 - plain[i] * 0.12 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7444) * 0.05);
|
||||
const barrier = mountainBarrierPenalty(x, y, "express");
|
||||
if (sea[i] || barrier >= INF) return INF;
|
||||
return Math.max(0.38, 1 + slope[i] * 13 + barrier + Math.max(0, elevation[i] - 0.58) * 20 + (river[i] > 0.5 ? 1.0 : river[i] * 0.5) - midDensityAffinity(x, y) * 0.42 - plain[i] * 0.14 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7444) * 0.05);
|
||||
}
|
||||
|
||||
function addEnvironmentalRing(city, mode, bucket, baseCost, existingPaths, targetRadius) {
|
||||
|
|
@ -2371,18 +2706,18 @@ export function generateMap(seedInput = 114514) {
|
|||
const expressRadius = roadRadius + 3 + rand(seed, city.x * 71 + city.y * 73) * 3;
|
||||
const railRadius = Math.max(8, roadRadius - 4);
|
||||
addEnvironmentalRing(city, "road", ringRoads, roadCost, [...nationalRoads, ...expressways, ...railways, ...branchRailways], roadRadius);
|
||||
const expressRingSegments = addEnvironmentalRing(city, "express", ringExpressways, expresswayCost, [...expressways, ...nationalRoads, ...railways, ...branchRailways], expressRadius);
|
||||
// Expressway rings are intentionally disabled; expressways stay as sparse interurban corridors.
|
||||
const railRingSegments = addEnvironmentalRing(city, "rail", ringRailways, railCost, [...railways, ...branchRailways, ...nationalRoads, ...expressways], railRadius);
|
||||
// Expressway rings should be rare; do not force a fallback ring when terrain rejects it.
|
||||
if (railRingSegments === 0) addLooseEnvironmentalRing(city, ringRailways, softRingRailCost, railRadius);
|
||||
}
|
||||
compactPathArray(ringExpressways, { minLength: 10, maxOverlap: 0.22, maxCount: 2 });
|
||||
ringExpressways.length = 0;
|
||||
compactPathArray(ringRoads, { minLength: 8, maxOverlap: 0.32, maxCount: 18 });
|
||||
compactPathArray(ringRailways, { minLength: 8, maxOverlap: 0.26, maxCount: 8 });
|
||||
|
||||
const gatewayCandidates = [];
|
||||
for (let x = 0; x < MAP_W; x++) for (const y of [0, MAP_H - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: y === 0 ? "N" : "S", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 }); }
|
||||
for (let y = 0; y < MAP_H; y++) for (const x of [0, MAP_W - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: x === 0 ? "W" : "E", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 }); }
|
||||
for (let x = 0; x < MAP_W; x++) for (const y of [0, MAP_H - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: y === 0 ? "N" : "S", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 + coastalLowland[i] * 0.2 - Math.max(0, elevation[i] - 0.56) * 1.6 - ridgeField[i] * 0.35 }); }
|
||||
for (let y = 0; y < MAP_H; y++) for (const x of [0, MAP_W - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: x === 0 ? "W" : "E", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 + coastalLowland[i] * 0.2 - Math.max(0, elevation[i] - 0.56) * 1.6 - ridgeField[i] * 0.35 }); }
|
||||
|
||||
let externalGateways = pickEntities(gatewayCandidates, {
|
||||
max: 2 + Math.floor(rand(seed, 1201) * 3),
|
||||
|
|
@ -2395,10 +2730,12 @@ export function generateMap(seedInput = 114514) {
|
|||
return (x, y) => {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) return INF;
|
||||
const borderPenalty = nearMapEdge(x, y, 1) && !(Math.abs(x - goal.x) <= 2 && Math.abs(y - goal.y) <= 2) ? 7 : nearMapEdge(x, y, 3) ? 1.5 : 0;
|
||||
const density = densityValue(x, y);
|
||||
const nodeAvoid = distanceToNearest(townAvoidNodes, x, y) < 2.2 ? 2.0 : 0;
|
||||
return Math.max(0.35, 1 + slope[i] * 10 + nodeAvoid + (river[i] > 0.45 ? 0.9 : 0) + floodplain[i] * 0.24 - density * 0.3 - plain[i] * 0.22 + borderPenalty + hash2(x, y, seed + 333) * 0.06);
|
||||
return Math.max(0.35, 1 + slope[i] * 13 + barrier + Math.max(0, elevation[i] - 0.54) * 7.5 + nodeAvoid + (river[i] > 0.45 ? 0.9 : 0) + floodplain[i] * 0.24 - density * 0.3 - plain[i] * 0.24 + borderPenalty + hash2(x, y, seed + 333) * 0.06);
|
||||
};
|
||||
}
|
||||
function externalExpresswayCost(goal) {
|
||||
|
|
@ -2476,13 +2813,13 @@ export function generateMap(seedInput = 114514) {
|
|||
if (paths[i].some(([x, y]) => elevation[indexOf(x, y)] > threshold)) paths.splice(i, 1);
|
||||
}
|
||||
}
|
||||
for (const paths of [railways, branchRailways, ringRailways, externalRailways, expressways, ringExpressways, externalExpressways]) pruneHighMountainTransport(paths, 0.82);
|
||||
for (const paths of [railways, branchRailways, ringRailways, externalRailways, expressways, externalExpressways]) pruneHighMountainTransport(paths, 0.82);
|
||||
|
||||
const expressInfluence = influenceFromPaths([...expressways, ...externalExpressways, ...ringExpressways], 6);
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...expressways, ...ringExpressways, ...externalRoads, ...externalExpressways], 4);
|
||||
const expressInfluence = influenceFromPaths([...expressways, ...externalExpressways], 6);
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...expressways, ...externalRoads, ...externalExpressways], 4);
|
||||
|
||||
const icCandidates = [];
|
||||
for (const path of [...expressways, ...ringExpressways, ...externalExpressways]) {
|
||||
for (const path of [...expressways, ...externalExpressways]) {
|
||||
icCandidates.push(...samplePath(path, 11 + Math.floor(rand(seed, path.length + 333) * 5)).map((p) => ({ ...p, score: 0.62 + plain[indexOf(p.x, p.y)] * 0.24 + midDensityAffinity(p.x, p.y) * 0.16, kind: "Interchange" })));
|
||||
for (const city of modernCities) {
|
||||
let best = null;
|
||||
|
|
@ -2599,8 +2936,10 @@ export function generateMap(seedInput = 114514) {
|
|||
|
||||
function minorRoadCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
return Math.max(0.3, 1 + slope[i] * 7.2 + floodplain[i] * 0.18 + (river[i] > 0.5 ? 1.0 : 0.18 * river[i]) - plain[i] * 0.22 - valleyField[i] * 0.34 - coastalLowland[i] * 0.12 + ridgeField[i] * 0.4 - roadNetInfluence[i] * 0.35 + normalEdgePenalty(x, y) + hash2(x, y, seed + 555) * 0.15);
|
||||
if (sea[i] || elevation[i] > 0.72) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "minor");
|
||||
if (barrier >= INF) return INF;
|
||||
return Math.max(0.3, 1 + slope[i] * 8.4 + barrier * 0.55 + Math.max(0, elevation[i] - 0.58) * 4.4 + floodplain[i] * 0.18 + (river[i] > 0.5 ? 1.0 : 0.18 * river[i]) - plain[i] * 0.24 - valleyField[i] * 0.36 - coastalLowland[i] * 0.12 + ridgeField[i] * 0.58 - roadNetInfluence[i] * 0.35 + normalEdgePenalty(x, y) + hash2(x, y, seed + 555) * 0.15);
|
||||
}
|
||||
|
||||
const connectedPairs = new Set();
|
||||
|
|
@ -2831,6 +3170,7 @@ export function generateMap(seedInput = 114514) {
|
|||
adminCentersRaw.push(...extra.filter((p) => adminCentersRaw.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 6)));
|
||||
}
|
||||
const adminId = generateAdminRegions(adminCentersRaw, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse);
|
||||
smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, 7);
|
||||
|
||||
function lockUrbanClusterToMunicipality(city, radius, allowSuburban = true) {
|
||||
if (!city || !prefectureMask[indexOf(city.x, city.y)]) return;
|
||||
|
|
@ -2887,7 +3227,27 @@ export function generateMap(seedInput = 114514) {
|
|||
}
|
||||
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 520);
|
||||
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);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 360);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 220);
|
||||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
||||
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
|
||||
// This keeps population figures proportional to the actually rendered urbanized area.
|
||||
recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence2);
|
||||
for (const city of modernCities) {
|
||||
if (city.isPrefecturalCapital) continue;
|
||||
const cap = cityPopulationCap(city);
|
||||
if (cap < INF && (city.population || 0) > cap) {
|
||||
city.population = Math.round(cap / 1000) * 1000;
|
||||
city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 100, 6, 16);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 400, 2.2, 5.2);
|
||||
city.urbanWeight = clamp(0.72 + Math.log10(Math.max(10000, city.population)) * 0.30, 1.0, 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
function makeHarborWorks(ports) {
|
||||
const out = [];
|
||||
for (const port of ports) {
|
||||
|
|
@ -2984,7 +3344,7 @@ export function generateMap(seedInput = 114514) {
|
|||
premodernRoads,
|
||||
minorRoads,
|
||||
modernCities,
|
||||
prefecturalCapital: modernCities.find((city) => city.isPrefecturalCapital) || modernCities[0] || null,
|
||||
prefecturalCapital: modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]) || null,
|
||||
totalPopulation: [...modernCities, ...satelliteCities].reduce((sum, city) => sum + (city.population || 0), 0),
|
||||
populationDensity,
|
||||
railways,
|
||||
|
|
|
|||
25
names.js
25
names.js
|
|
@ -2,30 +2,11 @@
|
|||
// Edit only this file to change generated names or override individual IDs.
|
||||
|
||||
export const NAME_PARTS = {
|
||||
prefixes: ["青", "白", "黒", "赤", "東", "西", "南", "北", "中", "大", "小", "浅", "高", "川", "山", "森", "松", "原", "桜", "海", "新", "古", "上", "下"],
|
||||
infixes: ["野", "原", "川", "浜", "崎", "丘", "谷", "峰", "浦", "沢", "島", "岡", "井", "瀬", "関", "橋", "口", "田", "津", "森", "台", "見"],
|
||||
suffixes: ["町", "市", "郷", "台", "丘", "浦", "港", "里", "野", "沢", "原", "橋", "森", "浜", "崎", "峠"],
|
||||
prefixes: ["青", "白", "黒", "赤", "東", "西", "南", "北", "中", "大", "小", "浅", "深", "高", "川", "山", "森", "松", "原", "桜", "海", "新", "古", "上", "下", "福", "岡", "宮", "伊", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "百", "千", "木", "林", "富", "住", "根", "葉", "由", "葦", "牟", "日", "月", "君",],
|
||||
infixes: ["野", "原", "川", "ヶ", "浜", "矢", "丘", "谷", "津", "浦", "沢", "島", "尾", "井", "瀬", "関", "橋", "口", "田", "津", "森", "阿", "見"],
|
||||
suffixes: ["島", "郷", "台", "丘", "浦", "吉", "里", "野", "沢", "原", "橋", "森", "浜", "呂", "峠"],
|
||||
};
|
||||
|
||||
export const KIND_SUFFIXES = {
|
||||
city: " City",
|
||||
port: " Port",
|
||||
market: " Town",
|
||||
village: " Village",
|
||||
castle: " Castle",
|
||||
castleTown: " Castle Town",
|
||||
castleRuin: " Castle Ruins",
|
||||
station: " Station",
|
||||
interchange: "IC",
|
||||
industrial: " Industrial Park",
|
||||
logistics: " Logistics Park",
|
||||
newtown: " New Town",
|
||||
satellite: " Satellite City",
|
||||
pass: " Pass",
|
||||
crossing: " Crossing",
|
||||
gateway: " Gate",
|
||||
admin: " District",
|
||||
};
|
||||
|
||||
export const CUSTOM_NAMES = {
|
||||
// "city-0": "Aohara",
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
|||
const eR = fieldSample(map.elevation, fx + 0.6, fy);
|
||||
const eU = fieldSample(map.elevation, fx, fy - 0.6);
|
||||
const eD = fieldSample(map.elevation, fx, fy + 0.6);
|
||||
const shade = clamp(0.9 + (eL - eR) * 1.0 + (eU - eD) * 0.65, 0.72, 1.18);
|
||||
const shade = clamp(0.9 + (eR - eL) * 1.0 + (eD - eU) * 0.65, 0.72, 1.18);
|
||||
const elevation = fieldSample(map.elevation, fx, fy);
|
||||
const contour = Math.abs((elevation * 16) - Math.round(elevation * 16));
|
||||
const majorContour = Math.abs((elevation * 8) - Math.round(elevation * 8));
|
||||
|
|
@ -440,7 +440,6 @@ export function drawMap(canvas, map, options) {
|
|||
for (const path of map.icAccessRoads || []) drawPath(ctx, path, "rgba(255, 230, 150, 0.82)", 1.45);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.0);
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.0);
|
||||
for (const path of map.ringExpressways || []) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 3.3);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.5);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Prefecture Map Generator v7 Tests</title>
|
||||
<title>Prefecture Map Generator v17 Tests</title>
|
||||
<style>
|
||||
body { font-family: ui-sans-serif, system-ui, sans-serif; background: #111; color: #eee; padding: 24px; }
|
||||
pre { background: #222; border-radius: 8px; padding: 16px; white-space: pre-wrap; }
|
||||
|
|
|
|||
16
test.js
16
test.js
|
|
@ -62,6 +62,18 @@ try {
|
|||
if (map.elevation[i] > 0.82) railExpressHighMountainCells += 1;
|
||||
}
|
||||
}
|
||||
let trunkHighElevationCells = 0;
|
||||
for (const path of modernPaths) {
|
||||
for (const [x, y] of path) {
|
||||
if (map.elevation[indexOf(x, y)] > 0.72) trunkHighElevationCells += 1;
|
||||
}
|
||||
}
|
||||
const flatPlainCells = [...map.plain].filter((value, i) => !map.sea[i] && value > 0.72 && map.slope[i] < 0.12).length;
|
||||
const largeMountainCities = map.modernCities.filter((city) => {
|
||||
const i = indexOf(city.x, city.y);
|
||||
return (city.population || 0) >= 250000 && (map.elevation[i] > 0.66 || map.plain[i] < 0.18 || map.slope[i] > 0.88);
|
||||
});
|
||||
const capitalInside = map.prefecturalCapital && map.prefectureMask[indexOf(map.prefecturalCapital.x, map.prefecturalCapital.y)];
|
||||
|
||||
assert(map.elevation.length === size, "elevation length matches map size");
|
||||
assert(map.sea.length === size, "sea length matches map size");
|
||||
|
|
@ -130,6 +142,10 @@ try {
|
|||
assert(elevationStdDev > 0.18, "terrain relief has sufficient contrast");
|
||||
assert(maxCoastalElevationStep < 0.12, "coastline and elevation do not create cliff artifacts");
|
||||
assert(railExpressHighMountainCells === 0, "railways and expressways avoid huge mountain cells");
|
||||
assert(trunkHighElevationCells === 0, "trunk roads, railways, and expressways avoid high-elevation cells");
|
||||
assert(flatPlainCells >= 160, "broad flat plains exist as actual low-slope cells");
|
||||
assert(largeMountainCities.length === 0, "large cities are not placed on unsuitable mountain sites");
|
||||
assert(capitalInside, "prefectural capital is inside the prefecture");
|
||||
assert(maxModernEndpointDegree <= 10, "modern transport endpoints are not over-centralized");
|
||||
assert(map.entitiesForNames.every((item) => item.id && item.name), "nameable entities have ids and names");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue