city tweaks
This commit is contained in:
parent
15c1031d72
commit
d762a88b22
12 changed files with 255 additions and 195 deletions
52
app.js
52
app.js
|
|
@ -1,11 +1,10 @@
|
||||||
import { generateMap } from "./mapGenerator.js";
|
import { generateMapAsync } from "./mapGenerator.js";
|
||||||
import { drawMap } from "./renderer.js";
|
import { drawMap } from "./renderer.js";
|
||||||
import { landuseLabel } from "./landuseCodes.js";
|
import { landuseLabel } from "./landuseCodes.js";
|
||||||
|
|
||||||
const modes = [
|
const modes = [
|
||||||
["all", "All"],
|
["all", "All"],
|
||||||
["terrain", "Terrain"],
|
["terrain", "Terrain"],
|
||||||
["suitability", "Suitability"],
|
|
||||||
["history", "Premodern"],
|
["history", "Premodern"],
|
||||||
["modern", "Modern"],
|
["modern", "Modern"],
|
||||||
["development", "Development"],
|
["development", "Development"],
|
||||||
|
|
@ -30,11 +29,13 @@ const showFeaturesInput = document.getElementById("showFeatures");
|
||||||
const showLabelsInput = document.getElementById("showLabels");
|
const showLabelsInput = document.getElementById("showLabels");
|
||||||
const modeGrid = document.getElementById("modeGrid");
|
const modeGrid = document.getElementById("modeGrid");
|
||||||
const statsEl = document.getElementById("stats");
|
const statsEl = document.getElementById("stats");
|
||||||
const idsEl = document.getElementById("nameIds");
|
|
||||||
const tooltipEl = document.getElementById("mapTooltip");
|
const tooltipEl = document.getElementById("mapTooltip");
|
||||||
const progressEl = document.getElementById("generationProgress");
|
const progressEl = document.getElementById("generationProgress");
|
||||||
const progressStageEl = document.getElementById("generationProgressStage");
|
const progressStageEl = document.getElementById("generationProgressStage");
|
||||||
const progressTimingsEl = document.getElementById("generationProgressTimings");
|
const progressTimingsEl = document.getElementById("generationProgressTimings");
|
||||||
|
let generationStartedAt = 0;
|
||||||
|
let generationCurrentStage = "";
|
||||||
|
let generationTimer = null;
|
||||||
|
|
||||||
function parseSeed(seedText) {
|
function parseSeed(seedText) {
|
||||||
const numeric = Number.parseInt(seedText, 10);
|
const numeric = Number.parseInt(seedText, 10);
|
||||||
|
|
@ -80,10 +81,12 @@ function renderTimingRows(timings = []) {
|
||||||
function updateGenerationProgress(event) {
|
function updateGenerationProgress(event) {
|
||||||
if (!progressEl) return;
|
if (!progressEl) return;
|
||||||
progressEl.classList.remove("hidden");
|
progressEl.classList.remove("hidden");
|
||||||
|
if (event?.status === "start") generationCurrentStage = event.label || "Preparing";
|
||||||
if (progressStageEl) {
|
if (progressStageEl) {
|
||||||
|
const elapsed = generationStartedAt ? ` / elapsed ${formatMs(performance.now() - generationStartedAt)}` : "";
|
||||||
progressStageEl.textContent = event?.status === "done"
|
progressStageEl.textContent = event?.status === "done"
|
||||||
? `Completed: ${event.label} / ${formatMs(event.ms)}`
|
? `Completed: ${event.label} / ${formatMs(event.ms)}${elapsed}`
|
||||||
: `Running: ${event?.label || "Preparing"}`;
|
: `Running: ${event?.label || generationCurrentStage || "Preparing"}${elapsed}`;
|
||||||
}
|
}
|
||||||
renderTimingRows(event?.timings || []);
|
renderTimingRows(event?.timings || []);
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +94,19 @@ function updateGenerationProgress(event) {
|
||||||
function setProgressVisible(visible, message = "Preparing") {
|
function setProgressVisible(visible, message = "Preparing") {
|
||||||
if (!progressEl) return;
|
if (!progressEl) return;
|
||||||
progressEl.classList.toggle("hidden", !visible);
|
progressEl.classList.toggle("hidden", !visible);
|
||||||
|
if (visible) {
|
||||||
|
generationStartedAt = performance.now();
|
||||||
|
generationCurrentStage = message;
|
||||||
|
if (generationTimer) window.clearInterval(generationTimer);
|
||||||
|
generationTimer = window.setInterval(() => {
|
||||||
|
if (progressStageEl && !progressEl.classList.contains("hidden")) {
|
||||||
|
progressStageEl.textContent = `Running: ${generationCurrentStage || "Preparing"} / elapsed ${formatMs(performance.now() - generationStartedAt)}`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else if (generationTimer) {
|
||||||
|
window.clearInterval(generationTimer);
|
||||||
|
generationTimer = null;
|
||||||
|
}
|
||||||
if (progressStageEl) progressStageEl.textContent = message;
|
if (progressStageEl) progressStageEl.textContent = message;
|
||||||
if (visible) renderTimingRows([]);
|
if (visible) renderTimingRows([]);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +119,8 @@ function getStats(map) {
|
||||||
return [
|
return [
|
||||||
["Map Type", map.terrainTemplate?.terrainTypeLabel || map.terrainDebug?.terrainTypeLabel || "-"],
|
["Map Type", map.terrainTemplate?.terrainTypeLabel || map.terrainDebug?.terrainTypeLabel || "-"],
|
||||||
["Terrain ID", map.terrainTemplate?.terrainType || map.terrainDebug?.terrainType || "-"],
|
["Terrain ID", map.terrainTemplate?.terrainType || map.terrainDebug?.terrainType || "-"],
|
||||||
["Generation Time", map.generationTotalMs ? `${formatMs(map.generationTotalMs)} / slowest ${(map.generationTimings || []).slice().sort((a, b) => b.ms - a.ms)[0]?.label || "-"}` : "-"],
|
["Generation Total", map.generationTotalMs ? formatMs(map.generationTotalMs) : "-"],
|
||||||
|
...(map.generationTimings || []).map((row) => [`Time: ${row.label}`, formatMs(row.ms)]),
|
||||||
["Villages", countText(map.villages)],
|
["Villages", countText(map.villages)],
|
||||||
["Market Towns", countText(map.markets)],
|
["Market Towns", countText(map.markets)],
|
||||||
["Castles", countText(map.castles)],
|
["Castles", countText(map.castles)],
|
||||||
|
|
@ -120,7 +137,6 @@ function getStats(map) {
|
||||||
["Population", (map.totalPopulation || 0).toLocaleString()],
|
["Population", (map.totalPopulation || 0).toLocaleString()],
|
||||||
["Rivers", `${map.mainRivers.length} main / ${(map.tributaryRivers || []).length} tributary / ${(map.smallStreams || []).length} hidden streams`],
|
["Rivers", `${map.mainRivers.length} main / ${(map.tributaryRivers || []).length} tributary / ${(map.smallStreams || []).length} hidden streams`],
|
||||||
["Neighbor Prefecture Borders", (map.regionalPrefectureBorders || []).length],
|
["Neighbor Prefecture Borders", (map.regionalPrefectureBorders || []).length],
|
||||||
["Harbor Works", (map.harborWorks || []).length],
|
|
||||||
["Rail Lines", map.railways.length + map.branchRailways.length + (map.ringRailways || []).length + map.externalRailways.length],
|
["Rail Lines", map.railways.length + map.branchRailways.length + (map.ringRailways || []).length + map.externalRailways.length],
|
||||||
["Industrial Zones", countText(map.industrialZones)],
|
["Industrial Zones", countText(map.industrialZones)],
|
||||||
["National Roads", `${map.nationalRoads.length} / pop cover ${Math.round((map.transportDebug?.nationalRoadPopulationCoverage || 0) * 100)}% / uncovered ${(map.transportDebug?.nationalRoadUncoveredPopulation || 0).toLocaleString()}`],
|
["National Roads", `${map.nationalRoads.length} / pop cover ${Math.round((map.transportDebug?.nationalRoadPopulationCoverage || 0) * 100)}% / uncovered ${(map.transportDebug?.nationalRoadUncoveredPopulation || 0).toLocaleString()}`],
|
||||||
|
|
@ -153,25 +169,6 @@ function renderStats(map) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNameIds(map) {
|
|
||||||
idsEl.innerHTML = "";
|
|
||||||
for (const entity of map.entitiesForNames.slice(0, 120)) {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "id-row";
|
|
||||||
|
|
||||||
const code = document.createElement("code");
|
|
||||||
code.textContent = entity.id;
|
|
||||||
|
|
||||||
const name = document.createElement("span");
|
|
||||||
const population = entity.population ? ` / ${entity.population.toLocaleString()} people` : "";
|
|
||||||
name.textContent = `${entity.name} / ${entity.kind}${population}`;
|
|
||||||
|
|
||||||
row.append(code, name);
|
|
||||||
idsEl.append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function nearestEntity(map, x, y, maxDistance = 5) {
|
function nearestEntity(map, x, y, maxDistance = 5) {
|
||||||
const groups = [
|
const groups = [
|
||||||
...(map.modernCities || []),
|
...(map.modernCities || []),
|
||||||
|
|
@ -251,9 +248,8 @@ async function regenerate() {
|
||||||
setProgressVisible(true, "Preparing generation...");
|
setProgressVisible(true, "Preparing generation...");
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
try {
|
try {
|
||||||
state.map = generateMap(parseSeed(state.seedText), { onProgress: updateGenerationProgress });
|
state.map = await generateMapAsync(parseSeed(state.seedText), { onProgress: updateGenerationProgress });
|
||||||
renderStats(state.map);
|
renderStats(state.map);
|
||||||
renderNameIds(state.map);
|
|
||||||
redraw();
|
redraw();
|
||||||
if (progressStageEl) progressStageEl.textContent = `Done in ${formatMs(state.map.generationTotalMs || 0)}`;
|
if (progressStageEl) progressStageEl.textContent = `Done in ${formatMs(state.map.generationTotalMs || 0)}`;
|
||||||
renderTimingRows(state.map.generationTimings || []);
|
renderTimingRows(state.map.generationTimings || []);
|
||||||
|
|
|
||||||
16
index.html
16
index.html
|
|
@ -14,8 +14,8 @@
|
||||||
<div>
|
<div>
|
||||||
<h1>Prefecture Map Generator v17</h1>
|
<h1>Prefecture Map Generator v17</h1>
|
||||||
<p>
|
<p>
|
||||||
Terrain-highlighted prefecture generation with terrain-snapped municipalities, a clear prefectural capital,
|
Multi-prefecture terrain generation with terrain-snapped municipalities, human geography layers,
|
||||||
hidden small streams, city-seeking national roads, and hover tooltips.
|
hidden small streams, trunk roads, and hover tooltips.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -58,17 +58,6 @@
|
||||||
<div id="stats" class="stats"></div>
|
<div id="stats" class="stats"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card legend">
|
|
||||||
<div class="card-title">Name Override IDs</div>
|
|
||||||
<p>Add entries to <code>names.js</code> in <code>CUSTOM_NAMES</code>.</p>
|
|
||||||
<pre class="example">export const CUSTOM_NAMES = {
|
|
||||||
"city-0": "CA",
|
|
||||||
"port-0": "PB",
|
|
||||||
"castle-0": "KC"
|
|
||||||
};</pre>
|
|
||||||
<div id="nameIds" class="id-list"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card legend">
|
<section class="card legend">
|
||||||
<div class="card-title">Legend</div>
|
<div class="card-title">Legend</div>
|
||||||
<div class="legend-grid" aria-label="Map legend">
|
<div class="legend-grid" aria-label="Map legend">
|
||||||
|
|
@ -85,7 +74,6 @@
|
||||||
<div class="legend-row"><span class="legend-icon satellite-icon"></span><span>Satellite city</span></div>
|
<div class="legend-row"><span class="legend-icon satellite-icon"></span><span>Satellite city</span></div>
|
||||||
<div class="legend-row"><span class="legend-icon station-icon"></span><span>Station</span></div>
|
<div class="legend-row"><span class="legend-icon station-icon"></span><span>Station</span></div>
|
||||||
<div class="legend-row"><span class="legend-icon industry-icon"></span><span>Industry / logistics</span></div>
|
<div class="legend-row"><span class="legend-icon industry-icon"></span><span>Industry / logistics</span></div>
|
||||||
<div class="legend-row"><span class="legend-line harbor-line"></span><span>Harbor works</span></div>
|
|
||||||
<div class="legend-row"><span class="legend-icon newtown-icon"></span><span>New town</span></div>
|
<div class="legend-row"><span class="legend-icon newtown-icon"></span><span>New town</span></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const LANDUSE_LABELS = Object.freeze({
|
||||||
[LANDUSE.INDUSTRIAL]: "Industrial zone",
|
[LANDUSE.INDUSTRIAL]: "Industrial zone",
|
||||||
[LANDUSE.LOGISTICS]: "Logistics area",
|
[LANDUSE.LOGISTICS]: "Logistics area",
|
||||||
[LANDUSE.NEW_TOWN]: "New town",
|
[LANDUSE.NEW_TOWN]: "New town",
|
||||||
[LANDUSE.ROADSIDE]: "Roadside development",
|
[LANDUSE.ROADSIDE]: "Suburban urban area",
|
||||||
[LANDUSE.FOREST]: "Forest / mountain land",
|
[LANDUSE.FOREST]: "Forest / mountain land",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,17 +287,16 @@ function restoreSurvivedSeedsByCompartment(adminId, prefectureMask, sea, compart
|
||||||
}
|
}
|
||||||
|
|
||||||
function municipalityCountBoundsForRegion(landCells, meta = {}) {
|
function municipalityCountBoundsForRegion(landCells, meta = {}) {
|
||||||
const focused = meta.isFocusedRegion !== false;
|
// Use the same administrative density curve for the highlighted prefecture
|
||||||
if (focused) return { min: 20, max: 50 };
|
// and neighboring prefectures. Only clipped slivers get a low floor.
|
||||||
// Neighbor prefectures are often visible only as clipped map-edge slivers.
|
|
||||||
// Avoid giving every tiny visible fragment the full 20-municipality floor.
|
|
||||||
let min = 1;
|
let min = 1;
|
||||||
if (landCells >= 500) min = 2;
|
if (landCells >= 420) min = 2;
|
||||||
if (landCells >= 950) min = 3;
|
if (landCells >= 850) min = 3;
|
||||||
if (landCells >= 1700) min = 5;
|
if (landCells >= 1500) min = 5;
|
||||||
if (landCells >= 2800) min = 7;
|
if (landCells >= 2500) min = 8;
|
||||||
if (landCells >= 4300) min = 10;
|
if (landCells >= 3800) min = 12;
|
||||||
const max = clamp(Math.round(landCells / 260 + 2), Math.max(min, 2), 34);
|
if (landCells >= 5600) min = 16;
|
||||||
|
const max = clamp(Math.round(landCells / 230 + 4), Math.max(min, 3), 46);
|
||||||
return { min, max };
|
return { min, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,7 +388,7 @@ function buildLowlandAdminSeeds({
|
||||||
const realSeeds = [];
|
const realSeeds = [];
|
||||||
for (const city of modernCities || []) {
|
for (const city of modernCities || []) {
|
||||||
if (!validLowlandPoint(city, !city.isPrefecturalCapital)) continue;
|
if (!validLowlandPoint(city, !city.isPrefecturalCapital)) continue;
|
||||||
if (!city.isPrefecturalCapital && (city.population || 0) < 85000) continue;
|
if ((city.population || 0) < 45000) continue;
|
||||||
const i = indexOf(city.x, city.y);
|
const i = indexOf(city.x, city.y);
|
||||||
realSeeds.push({ x: city.x, y: city.y, score: 1.35 + (city.population || 0) / 650000 + lowlandAdminSeedScore(i, fields), population: city.population || 0, protectedCity: city, seedKind: city.isPrefecturalCapital ? "capital" : "modernCity" });
|
realSeeds.push({ x: city.x, y: city.y, score: 1.35 + (city.population || 0) / 650000 + lowlandAdminSeedScore(i, fields), population: city.population || 0, protectedCity: city, seedKind: city.isPrefecturalCapital ? "capital" : "modernCity" });
|
||||||
}
|
}
|
||||||
|
|
@ -647,12 +646,8 @@ function generateAdminLayoutForMask({
|
||||||
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta });
|
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta });
|
||||||
const compartmentMultiplier = clamp(4.6 + rand(seed, 1320) * 2.4, 4.6, 7.0);
|
const compartmentMultiplier = clamp(4.6 + rand(seed, 1320) * 2.4, 4.6, 7.0);
|
||||||
const regionLandArea = adminRegionMeta.landArea || maskLandArea(prefectureMask, sea);
|
const regionLandArea = adminRegionMeta.landArea || maskLandArea(prefectureMask, sea);
|
||||||
const minCompartmentTarget = adminRegionMeta.isFocusedRegion === false
|
const minCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 3.4, regionLandArea / 78)), 22, 150);
|
||||||
? clamp(Math.round(Math.max(targetMunicipalityCount * 3.2, regionLandArea / 75)), 18, 90)
|
const maxCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 6.0, regionLandArea / 36)), minCompartmentTarget, 320);
|
||||||
: 120;
|
|
||||||
const maxCompartmentTarget = adminRegionMeta.isFocusedRegion === false
|
|
||||||
? clamp(Math.round(Math.max(targetMunicipalityCount * 5.8, regionLandArea / 38)), minCompartmentTarget, 220)
|
|
||||||
: 360;
|
|
||||||
let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), minCompartmentTarget, maxCompartmentTarget);
|
let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), minCompartmentTarget, maxCompartmentTarget);
|
||||||
let adminCentersRaw = buildLowlandAdminSeeds({
|
let adminCentersRaw = buildLowlandAdminSeeds({
|
||||||
seed,
|
seed,
|
||||||
|
|
@ -678,7 +673,7 @@ function generateAdminLayoutForMask({
|
||||||
newTowns,
|
newTowns,
|
||||||
stations,
|
stations,
|
||||||
});
|
});
|
||||||
if (adminCentersRaw.length < 20) targetCompartmentCount = Math.max(targetCompartmentCount, minCompartmentTarget);
|
if (adminCentersRaw.length < targetMunicipalityCount) targetCompartmentCount = Math.max(targetCompartmentCount, minCompartmentTarget);
|
||||||
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "natural compartments", targetMunicipalityCount, targetCompartmentCount, seedCount: adminCentersRaw.length });
|
adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "natural compartments", targetMunicipalityCount, targetCompartmentCount, seedCount: adminCentersRaw.length });
|
||||||
const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, {
|
const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, {
|
||||||
seed,
|
seed,
|
||||||
|
|
@ -843,7 +838,7 @@ function generateAdminLayoutForMask({
|
||||||
splitMunicipalities: 0,
|
splitMunicipalities: 0,
|
||||||
rejectedMunicipalities: 0,
|
rejectedMunicipalities: 0,
|
||||||
skippedDuplicateCompartmentRebuild: true,
|
skippedDuplicateCompartmentRebuild: true,
|
||||||
skippedForVisibleFragment: adminRegionMeta.isFocusedRegion === false && regionLandArea < 6500,
|
skippedForVisibleFragment: false,
|
||||||
};
|
};
|
||||||
adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells;
|
adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells;
|
||||||
adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities;
|
adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities;
|
||||||
|
|
@ -1048,7 +1043,7 @@ export function generateAdminLayout(context) {
|
||||||
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
|
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
|
||||||
const minFullAdminRegionArea = 1500;
|
const minFullAdminRegionArea = 1500;
|
||||||
const regionIds = discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)
|
const regionIds = discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)
|
||||||
.filter((regionId) => regionId === 0 || (regionId !== OUTER_ANCHOR_REGION_ID && maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= minFullAdminRegionArea));
|
.filter((regionId) => regionId !== OUTER_ANCHOR_REGION_ID && maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= minFullAdminRegionArea);
|
||||||
|
|
||||||
if (!prefectureRegionId || regionIds.length <= 1) return generateAdminLayoutForMask(context);
|
if (!prefectureRegionId || regionIds.length <= 1) return generateAdminLayoutForMask(context);
|
||||||
|
|
||||||
|
|
@ -1063,7 +1058,7 @@ export function generateAdminLayout(context) {
|
||||||
for (const regionId of regionIds) {
|
for (const regionId of regionIds) {
|
||||||
const regionMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
|
const regionMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
|
||||||
const regionArea = maskLandArea(regionMask, sea);
|
const regionArea = maskLandArea(regionMask, sea);
|
||||||
if (regionId !== 0 && regionArea < minFullAdminRegionArea) continue;
|
if (regionArea < minFullAdminRegionArea) continue;
|
||||||
|
|
||||||
const localContext = {
|
const localContext = {
|
||||||
...context,
|
...context,
|
||||||
|
|
@ -1081,7 +1076,7 @@ export function generateAdminLayout(context) {
|
||||||
adminRegionMeta: {
|
adminRegionMeta: {
|
||||||
regionId,
|
regionId,
|
||||||
landArea: regionArea,
|
landArea: regionArea,
|
||||||
isFocusedRegion: regionId === 0,
|
isFocusedRegion: true,
|
||||||
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
|
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
|
||||||
},
|
},
|
||||||
adminProgress,
|
adminProgress,
|
||||||
|
|
|
||||||
|
|
@ -3030,7 +3030,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
else if (newTownInfluence[i] > 0.42 && urbanEnvelope > 0.16) landuse[i] = LANDUSE.NEW_TOWN;
|
else if (newTownInfluence[i] > 0.42 && urbanEnvelope > 0.16) landuse[i] = LANDUSE.NEW_TOWN;
|
||||||
else if (urbanFootprint[i]) landuse[i] = LANDUSE.SUBURB;
|
else if (urbanFootprint[i]) landuse[i] = LANDUSE.SUBURB;
|
||||||
else if (suburbScore > 0.235 && !isolatedCorridor && slope[i] < 0.32 && ridgeField[i] < 0.48 && (normalizedUrbanDistance < 1.42 || satelliteInfluence[i] > 0.24)) landuse[i] = LANDUSE.SUBURB;
|
else if (suburbScore > 0.235 && !isolatedCorridor && slope[i] < 0.32 && ridgeField[i] < 0.48 && (normalizedUrbanDistance < 1.42 || satelliteInfluence[i] > 0.24)) landuse[i] = LANDUSE.SUBURB;
|
||||||
else if (roadsideScore > 0.5 && plain[i] > 0.18 && slope[i] < 0.34 && ridgeField[i] < 0.5 && !isolatedCorridor && (interchangeInfluence[i] > 0.24 || logisticsInfluence[i] > 0.16 || cityInfluence[i] > 0.09)) landuse[i] = LANDUSE.ROADSIDE;
|
else if (roadsideScore > 0.5 && plain[i] > 0.18 && slope[i] < 0.34 && ridgeField[i] < 0.5 && !isolatedCorridor && (interchangeInfluence[i] > 0.24 || logisticsInfluence[i] > 0.16 || cityInfluence[i] > 0.09)) landuse[i] = LANDUSE.SUBURB;
|
||||||
else if (farm) landuse[i] = LANDUSE.FARMLAND;
|
else if (farm) landuse[i] = LANDUSE.FARMLAND;
|
||||||
else if (ruralSettlementFootprint[i] || ruralScore > 0.3) landuse[i] = LANDUSE.RURAL;
|
else if (ruralSettlementFootprint[i] || ruralScore > 0.3) landuse[i] = LANDUSE.RURAL;
|
||||||
else landuse[i] = LANDUSE.RURAL;
|
else landuse[i] = LANDUSE.RURAL;
|
||||||
|
|
|
||||||
133
mapFeaturesV2.js
133
mapFeaturesV2.js
|
|
@ -204,11 +204,10 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function visibilityFactor(regionId, st) {
|
function visibilityFactor(regionId, st) {
|
||||||
if (regionId === 0) return 1.15;
|
|
||||||
if (!st || st.area <= 0) return 0;
|
if (!st || st.area <= 0) return 0;
|
||||||
// Small map-edge slivers should not get the same municipal/human density
|
// Treat the focused prefecture and neighboring prefectures with the same
|
||||||
// as full neighboring prefectures. This keeps external regions legible.
|
// density curve. Only genuinely clipped map-edge slivers are downscaled.
|
||||||
return clamp(Math.sqrt(st.area / 1700), 0.28, 0.92);
|
return clamp(Math.sqrt(st.area / 1900), 0.32, 1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickRegionalPoints(scoreArray, {
|
function pickRegionalPoints(scoreArray, {
|
||||||
|
|
@ -316,8 +315,8 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
if (!st || st.developableCells < 10) return 0;
|
if (!st || st.developableCells < 10) return 0;
|
||||||
const vf = visibilityFactor(regionId, st);
|
const vf = visibilityFactor(regionId, st);
|
||||||
const raw = (st.developableCells / 65 + st.valleyCells / 44 + st.coastCells / 55 + 1.2) * vf;
|
const raw = (st.developableCells / 65 + st.valleyCells / 44 + st.coastCells / 55 + 1.2) * vf;
|
||||||
const min = regionId === 0 ? 10 : st.area > 1100 ? 3 : st.area > 280 ? 1 : 0;
|
const min = st.area > 2600 ? 7 : st.area > 1400 ? 4 : st.area > 520 ? 2 : st.area > 220 ? 1 : 0;
|
||||||
const max = regionId === 0 ? 30 : st.area > 1800 ? 13 : st.area > 600 ? 7 : 3;
|
const max = st.area > 3600 ? 24 : st.area > 2200 ? 17 : st.area > 900 ? 9 : 4;
|
||||||
return Math.round(clamp(raw + rand(seed, 1033 + regionId * 19) * 1.5, min, max));
|
return Math.round(clamp(raw + rand(seed, 1033 + regionId * 19) * 1.5, min, max));
|
||||||
},
|
},
|
||||||
}).map((p, n) => {
|
}).map((p, n) => {
|
||||||
|
|
@ -365,8 +364,8 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
if (!st || st.townCells < 8) return 0;
|
if (!st || st.townCells < 8) return 0;
|
||||||
const vf = visibilityFactor(regionId, st);
|
const vf = visibilityFactor(regionId, st);
|
||||||
const raw = (st.developableCells / 260 + st.valleyCells / 150 + st.coastCells / 160 + 0.8) * vf;
|
const raw = (st.developableCells / 260 + st.valleyCells / 150 + st.coastCells / 160 + 0.8) * vf;
|
||||||
const min = regionId === 0 ? 4 : st.area > 1300 ? 1 : 0;
|
const min = st.area > 2600 ? 3 : st.area > 1200 ? 2 : st.area > 520 ? 1 : 0;
|
||||||
const max = regionId === 0 ? 11 : st.area > 1800 ? 5 : st.area > 650 ? 3 : 1;
|
const max = st.area > 3600 ? 9 : st.area > 2200 ? 7 : st.area > 800 ? 4 : 2;
|
||||||
return Math.round(clamp(raw + rand(seed, 1043 + regionId * 23) * 0.8, min, max));
|
return Math.round(clamp(raw + rand(seed, 1043 + regionId * 23) * 0.8, min, max));
|
||||||
},
|
},
|
||||||
extraScore: (x, y, i) => (distanceToNearest(commercialPorts, x, y) < 8 ? 0.07 : 0) + confluenceField[i] * 0.08,
|
extraScore: (x, y, i) => (distanceToNearest(commercialPorts, x, y) < 8 ? 0.07 : 0) + confluenceField[i] * 0.08,
|
||||||
|
|
@ -444,7 +443,9 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
const i = indexOf(p.x, p.y);
|
const i = indexOf(p.x, p.y);
|
||||||
const regionId = regionIdAt(p.x, p.y);
|
const regionId = regionIdAt(p.x, p.y);
|
||||||
if (regionId < 0) continue;
|
if (regionId < 0) continue;
|
||||||
const capacity = estimateUrbanCapacity(p, regionId === 0 ? 30 : 24, regionId === 0 ? 1.12 : 1.0);
|
const st = regionStats.get(regionId);
|
||||||
|
const cityRadius = st && st.area > 2400 ? 28 : st && st.area > 900 ? 24 : 20;
|
||||||
|
const capacity = estimateUrbanCapacity(p, cityRadius, 1.0);
|
||||||
const score =
|
const score =
|
||||||
Math.log10(capacity + 1) * 0.72 +
|
Math.log10(capacity + 1) * 0.72 +
|
||||||
townSuitability[i] * 1.40 +
|
townSuitability[i] * 1.40 +
|
||||||
|
|
@ -463,12 +464,14 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
const st = regionStats.get(regionId);
|
const st = regionStats.get(regionId);
|
||||||
if (!st || st.developableCells < 30) continue;
|
if (!st || st.developableCells < 30) continue;
|
||||||
const vf = visibilityFactor(regionId, st);
|
const vf = visibilityFactor(regionId, st);
|
||||||
const maxCities = regionId === 0
|
const maxCities = clamp(
|
||||||
? clamp(Math.round(3 + st.developableCells / 520 + rand(seed, 12100) * 2), 5, 9)
|
Math.round((st.developableCells / 720 + 0.9) * vf + rand(seed, 12100 + regionId * 17) * 1.2),
|
||||||
: clamp(Math.round((st.developableCells / 850 + 0.8) * vf), st.area > 1500 ? 1 : 0, st.area > 2600 ? 4 : st.area > 950 ? 2 : 1);
|
st.area > 1600 ? 1 : 0,
|
||||||
|
st.area > 3600 ? 6 : st.area > 2200 ? 4 : st.area > 900 ? 3 : 1
|
||||||
|
);
|
||||||
const selected = pickEntities(list, {
|
const selected = pickEntities(list, {
|
||||||
max: maxCities,
|
max: maxCities,
|
||||||
minDistance: regionId === 0 ? 16 : 18,
|
minDistance: 17,
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
seed: seed + 12110 + regionId * 313,
|
seed: seed + 12110 + regionId * 313,
|
||||||
jitter: 0.02,
|
jitter: 0.02,
|
||||||
|
|
@ -480,61 +483,34 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modernCities.some((p) => inFocusedPrefecture(p))) {
|
// No focused-prefecture fallback: all prefecture regions use the same city
|
||||||
const focusCandidates = [...markets, ...commercialPorts, ...villages].filter((p) => inFocusedPrefecture(p));
|
// selection rules, so the highlighted region is not overwritten after the
|
||||||
let fallback = focusCandidates.sort((a, b) => {
|
// regional pass.
|
||||||
const ai = indexOf(a.x, a.y);
|
|
||||||
const bi = indexOf(b.x, b.y);
|
|
||||||
return (townSuitability[bi] + developable[bi]) - (townSuitability[ai] + developable[ai]);
|
|
||||||
})[0];
|
|
||||||
if (!fallback) {
|
|
||||||
let best = null;
|
|
||||||
let bestScore = -INF;
|
|
||||||
for (let y = 2; y < MAP_H - 2; y += 2) {
|
|
||||||
for (let x = 2; x < MAP_W - 2; x += 2) {
|
|
||||||
const i = indexOf(x, y);
|
|
||||||
if (!prefectureMask[i] || sea[i]) continue;
|
|
||||||
const score = townSuitability[i] + developable[i] + hash2(x, y, seed + 12199) * 0.04;
|
|
||||||
if (score > bestScore) { bestScore = score; best = { x, y, score, kind: "Local City", regionId: 0 }; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fallback = best;
|
|
||||||
}
|
|
||||||
if (fallback) modernCities.push({
|
|
||||||
...fallback,
|
|
||||||
candidateKind: fallback.candidateKind || "fallback",
|
|
||||||
score: fallback.score || 0.5,
|
|
||||||
capacity: estimateUrbanCapacity(fallback, 30, 1.15),
|
|
||||||
regionId: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
modernCities.sort((a, b) => b.capacity - a.capacity || b.score - a.score);
|
modernCities.sort((a, b) => b.capacity - a.capacity || b.score - a.score);
|
||||||
for (const [rank, city] of modernCities.entries()) {
|
for (const [rank, city] of modernCities.entries()) {
|
||||||
const isFocused = inFocusedPrefecture(city);
|
const isFirstInRegion = !modernCities.slice(0, rank).some((c) => c.regionId === city.regionId);
|
||||||
const isPrefecturalCapital = isFocused && !modernCities.slice(0, rank).some((c) => c.isPrefecturalCapital);
|
const isPrefecturalCapital = inFocusedPrefecture(city) && !modernCities.slice(0, rank).some((c) => c.isPrefecturalCapital);
|
||||||
const isRegionalCapital = !isFocused && !modernCities.slice(0, rank).some((c) => c.regionId === city.regionId && c.isRegionalCapital);
|
const isRegionalCapital = isFirstInRegion;
|
||||||
const rawPop = isPrefecturalCapital
|
const rawPop = isRegionalCapital
|
||||||
? 450000 + rand(seed, 12200) * 1150000
|
? 150000 + rand(seed, 12201 + city.regionId * 17) * 520000
|
||||||
: isRegionalCapital
|
: 32000 + Math.pow(rand(seed, 12202 + rank * 19 + city.x), 0.7) * 260000;
|
||||||
? 160000 + rand(seed, 12201 + city.regionId * 17) * 460000
|
const capMultiplier = isRegionalCapital ? 1.10 : 1.0;
|
||||||
: 32000 + Math.pow(rand(seed, 12202 + rank * 19 + city.x), 0.7) * 260000;
|
|
||||||
const capMultiplier = isPrefecturalCapital ? 1.22 : isRegionalCapital ? 1.08 : 1.0;
|
|
||||||
const population = Math.round(Math.min(rawPop, city.capacity * capMultiplier) / 1000) * 1000;
|
const population = Math.round(Math.min(rawPop, city.capacity * capMultiplier) / 1000) * 1000;
|
||||||
city.population = Math.max(isPrefecturalCapital ? 260000 : isRegionalCapital ? 90000 : 24000, population);
|
city.population = Math.max(isRegionalCapital ? 90000 : 24000, population);
|
||||||
city.isPrefecturalCapital = isPrefecturalCapital;
|
city.isPrefecturalCapital = isPrefecturalCapital;
|
||||||
city.isRegionalCapital = isRegionalCapital;
|
city.isRegionalCapital = isRegionalCapital;
|
||||||
city.rank = isPrefecturalCapital ? "Prefectural Capital" : isRegionalCapital ? "Regional Capital" : city.population >= 200000 ? "Regional City" : "Local City";
|
city.rank = isPrefecturalCapital ? "Prefectural Capital" : isRegionalCapital ? "Regional Capital" : city.population >= 200000 ? "Regional City" : "Local City";
|
||||||
city.kind = city.rank;
|
city.kind = city.rank;
|
||||||
city.urbanRadius = clamp(5.8 + Math.sqrt(city.population) / 72, 8, isPrefecturalCapital ? 40 : isRegionalCapital ? 32 : 24);
|
city.urbanRadius = clamp(5.8 + Math.sqrt(city.population) / 72, 8, isRegionalCapital ? 34 : 24);
|
||||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 380, 2.2, isPrefecturalCapital ? 8.5 : 6.5);
|
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 380, 2.2, isRegionalCapital ? 6.5 : 5.6);
|
||||||
city.sprawlRadius = clamp(city.urbanRadius * (isPrefecturalCapital ? 1.65 : isRegionalCapital ? 1.45 : city.population >= 120000 ? 1.28 : 1.15), city.urbanRadius + 2, isPrefecturalCapital ? 56 : isRegionalCapital ? 42 : 30);
|
city.sprawlRadius = clamp(city.urbanRadius * (isRegionalCapital ? 1.45 : city.population >= 120000 ? 1.28 : 1.15), city.urbanRadius + 2, isRegionalCapital ? 44 : 30);
|
||||||
city.urbanWeight = clamp(0.95 + Math.log10(Math.max(10000, city.population)) * 0.29, 1.08, 2.5);
|
city.urbanWeight = clamp(0.95 + Math.log10(Math.max(10000, city.population)) * 0.29, 1.08, 2.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cityPopulationCap(city) {
|
function cityPopulationCap(city) {
|
||||||
const radius = city?.isPrefecturalCapital ? 34 : city?.isRegionalCapital ? 29 : city?.population >= 350000 ? 26 : 18;
|
const radius = city?.isRegionalCapital ? 29 : city?.population >= 350000 ? 26 : 18;
|
||||||
const bias = city?.isPrefecturalCapital ? 1.25 : city?.isRegionalCapital ? 1.12 : 1.0;
|
const bias = city?.isRegionalCapital ? 1.12 : 1.0;
|
||||||
return estimateUrbanCapacity(city, radius, bias);
|
return estimateUrbanCapacity(city, radius, bias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,7 +559,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
...markets.filter(inRegion).map((p) => ({ ...p, nodeWeight: 3.2 + (p.population || 0) / 25000 })),
|
...markets.filter(inRegion).map((p) => ({ ...p, nodeWeight: 3.2 + (p.population || 0) / 25000 })),
|
||||||
...commercialPorts.filter(inRegion).map((p) => ({ ...p, nodeWeight: p.portClass === "major" ? 6.5 : 4.6 })),
|
...commercialPorts.filter(inRegion).map((p) => ({ ...p, nodeWeight: p.portClass === "major" ? 6.5 : 4.6 })),
|
||||||
...passes.filter(inRegion).map((p) => ({ ...p, nodeWeight: 2.2 })),
|
...passes.filter(inRegion).map((p) => ({ ...p, nodeWeight: 2.2 })),
|
||||||
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, regionId === 0 ? 18 : 10);
|
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 16 : 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
const premodernRoads = [];
|
const premodernRoads = [];
|
||||||
|
|
@ -615,7 +591,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
if (nodes.length < 2) continue;
|
if (nodes.length < 2) continue;
|
||||||
const connected = [nodes[0]];
|
const connected = [nodes[0]];
|
||||||
const remaining = nodes.slice(1);
|
const remaining = nodes.slice(1);
|
||||||
const maxEdges = regionId === 0 ? Math.min(14, nodes.length + 3) : Math.min(7, nodes.length + 1);
|
const maxEdges = (regionStats.get(regionId)?.area || 0) > 2200 ? Math.min(13, nodes.length + 3) : Math.min(7, nodes.length + 1);
|
||||||
while (remaining.length && nationalRoads.length < 48) {
|
while (remaining.length && nationalRoads.length < 48) {
|
||||||
let best = null;
|
let best = null;
|
||||||
let bestScore = INF;
|
let bestScore = INF;
|
||||||
|
|
@ -635,7 +611,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A few k-nearest shortcuts for urbanized regions.
|
// A few k-nearest shortcuts for urbanized regions.
|
||||||
const urbanNodes = nodes.filter((p) => p.population || p.portClass).slice(0, regionId === 0 ? 8 : 4);
|
const urbanNodes = nodes.filter((p) => p.population || p.portClass).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 7 : 4);
|
||||||
for (let i = 0; i < urbanNodes.length; i++) {
|
for (let i = 0; i < urbanNodes.length; i++) {
|
||||||
const a = urbanNodes[i];
|
const a = urbanNodes[i];
|
||||||
const b = urbanNodes.slice(i + 1).sort((p, q) => Math.hypot(a.x - p.x, a.y - p.y) - Math.hypot(a.x - q.x, a.y - q.y))[0];
|
const b = urbanNodes.slice(i + 1).sort((p, q) => Math.hypot(a.x - p.x, a.y - p.y) - Math.hypot(a.x - q.x, a.y - q.y))[0];
|
||||||
|
|
@ -645,7 +621,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Railways: only high-order cities/ports, as a lightweight placeholder.
|
// Railways: only high-order cities/ports, as a lightweight placeholder.
|
||||||
const railNodes = nodes.filter((p) => (p.population || 0) > 80000 || p.portClass === "major" || p.portClass === "regional").slice(0, regionId === 0 ? 7 : 4);
|
const railNodes = nodes.filter((p) => (p.population || 0) > 80000 || p.portClass === "major" || p.portClass === "regional").slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 6 : 4);
|
||||||
railNodes.sort((a, b) => a.x - b.x || a.y - b.y);
|
railNodes.sort((a, b) => a.x - b.x || a.y - b.y);
|
||||||
for (let i = 1; i < railNodes.length; i++) {
|
for (let i = 1; i < railNodes.length; i++) {
|
||||||
const path = routeLight(railNodes[i - 1], railNodes[i], 4);
|
const path = routeLight(railNodes[i - 1], railNodes[i], 4);
|
||||||
|
|
@ -672,7 +648,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
if (!sea[i] && regionIdAt(x, y) === regionId && (x < 5 || y < 5 || x > MAP_W - 6 || y > MAP_H - 6)) edgeCandidates.push({ x, y, score: developable[i] + valleySettlement[i] });
|
if (!sea[i] && regionIdAt(x, y) === regionId && (x < 5 || y < 5 || x > MAP_W - 6 || y > MAP_H - 6)) edgeCandidates.push({ x, y, score: developable[i] + valleySettlement[i] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const gateway = pickEntities(edgeCandidates, { max: regionId === 0 ? 2 : 1, minDistance: 16, seed: seed + 13200 + regionId * 11 })[0];
|
const gateway = pickEntities(edgeCandidates, { max: (regionStats.get(regionId)?.area || 0) > 2200 ? 2 : 1, minDistance: 16, seed: seed + 13200 + regionId * 11 })[0];
|
||||||
if (gateway) {
|
if (gateway) {
|
||||||
gateway.kind = "External Gateway";
|
gateway.kind = "External Gateway";
|
||||||
gateway.regionId = regionId;
|
gateway.regionId = regionId;
|
||||||
|
|
@ -696,7 +672,12 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads, ...expressways], 5);
|
// Land-use road influence intentionally excludes expressways. Expressways
|
||||||
|
// are through-corridors here, not automatic suburbanization generators.
|
||||||
|
// A narrow field controls land-use attachment, while a broader field raises
|
||||||
|
// population density around trunk roads without painting a wide suburb band.
|
||||||
|
const roadLanduseInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads], 2.25);
|
||||||
|
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads], 5.0);
|
||||||
const railInfluence2 = influenceFromPaths([...railways, ...branchRailways, ...externalRailways], 4);
|
const railInfluence2 = influenceFromPaths([...railways, ...branchRailways, ...externalRailways], 4);
|
||||||
|
|
||||||
const stations = [];
|
const stations = [];
|
||||||
|
|
@ -739,7 +720,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const city of modernCities) {
|
for (const city of modernCities) {
|
||||||
addKernel(cityInfluence, city, city.sprawlRadius || Math.round((city.urbanRadius || 10) * 1.4), (city.urbanWeight || 1.0) * (city.isPrefecturalCapital ? 0.46 : city.isRegionalCapital ? 0.38 : 0.30), 2.75, true, "add");
|
addKernel(cityInfluence, city, city.sprawlRadius || Math.round((city.urbanRadius || 10) * 1.4), (city.urbanWeight || 1.0) * (city.isRegionalCapital ? 0.38 : 0.30), 2.75, true, "add");
|
||||||
addKernel(cityInfluence, city, city.urbanRadius || 10, city.urbanWeight || 1.0, 1.18, true, "add");
|
addKernel(cityInfluence, city, city.urbanRadius || 10, city.urbanWeight || 1.0, 1.18, true, "add");
|
||||||
addKernel(coreInfluence, city, city.coreRadius || 3, (city.urbanWeight || 1.0) * 1.10, 1.65, true, "max");
|
addKernel(coreInfluence, city, city.coreRadius || 3, (city.urbanWeight || 1.0) * 1.10, 1.65, true, "max");
|
||||||
}
|
}
|
||||||
|
|
@ -786,8 +767,8 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
for (let x = 0; x < MAP_W; x++) {
|
for (let x = 0; x < MAP_W; x++) {
|
||||||
const i = indexOf(x, y);
|
const i = indexOf(x, y);
|
||||||
if (sea[i]) continue;
|
if (sea[i]) continue;
|
||||||
const transport = Math.max(roadInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
||||||
const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.26 + roadInfluence[i] * 0.12 + railInfluence2[i] * 0.10;
|
const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.26 + roadInfluence[i] * 0.14 + railInfluence2[i] * 0.10;
|
||||||
const core = coreInfluence[i];
|
const core = coreInfluence[i];
|
||||||
const oldTown = oldTownInfluence[i] * 0.70 + townInfluence[i] * 0.38;
|
const oldTown = oldTownInfluence[i] * 0.70 + townInfluence[i] * 0.38;
|
||||||
const rural = villageInfluence[i] * 0.24 + ruralSuitability[i] * 0.30;
|
const rural = villageInfluence[i] * 0.24 + ruralSuitability[i] * 0.30;
|
||||||
|
|
@ -798,13 +779,13 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
basinField[i] * 0.16 +
|
basinField[i] * 0.16 +
|
||||||
valleyField[i] * 0.16 +
|
valleyField[i] * 0.16 +
|
||||||
coastalLowland[i] * 0.12 +
|
coastalLowland[i] * 0.12 +
|
||||||
transport * 0.18 +
|
roadInfluence[i] * 0.16 + transport * 0.10 +
|
||||||
riverUrban * 0.14 -
|
riverUrban * 0.14 -
|
||||||
slope[i] * 0.18 -
|
slope[i] * 0.18 -
|
||||||
ridgeField[i] * 0.12 -
|
ridgeField[i] * 0.12 -
|
||||||
floodplain[i] * 0.08
|
floodplain[i] * 0.08
|
||||||
);
|
);
|
||||||
populationDensity[i] = clamp(urban * 0.66 + core * 0.46 + oldTown * 0.28 + townInfluence[i] * 0.16 + villageInfluence[i] * 0.14 + transport * 0.12);
|
populationDensity[i] = clamp(urban * 0.66 + core * 0.46 + oldTown * 0.28 + townInfluence[i] * 0.16 + villageInfluence[i] * 0.14 + roadInfluence[i] * 0.18 + transport * 0.08);
|
||||||
maxDensity = Math.max(maxDensity, populationDensity[i]);
|
maxDensity = Math.max(maxDensity, populationDensity[i]);
|
||||||
|
|
||||||
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
|
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
|
||||||
|
|
@ -825,13 +806,13 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const suburbanity = urban * 0.88 + transport * 0.23 + stationInfluence[i] * 0.12 + townInfluence[i] * 0.08 + riverUrban * 0.08;
|
const suburbanity = urban * 0.88 + transport * 0.23 + stationInfluence[i] * 0.12 + townInfluence[i] * 0.08 + riverUrban * 0.08;
|
||||||
const edgeTaper = clamp(cityInfluence[i] * 0.52 + stationInfluence[i] * 0.16 + roadInfluence[i] * 0.10 + 0.28);
|
const edgeTaper = clamp(cityInfluence[i] * 0.52 + stationInfluence[i] * 0.16 + roadLanduseInfluence[i] * 0.10 + 0.28);
|
||||||
const sprawlBias = clamp(0.58 + hash2(x, y, baseNoiseSeed) * 0.42);
|
const sprawlBias = clamp(0.58 + hash2(x, y, baseNoiseSeed) * 0.42);
|
||||||
const sprawlScore = suburbanity * sprawlBias * edgeTaper - core * 0.12;
|
const sprawlScore = suburbanity * sprawlBias * edgeTaper - core * 0.12;
|
||||||
if (sprawlScore > 0.24 && urbanCapacity[i] > 0.10) {
|
if (sprawlScore > 0.24 && urbanCapacity[i] > 0.10) {
|
||||||
landuse[i] = LANDUSE.SUBURB;
|
landuse[i] = LANDUSE.SUBURB;
|
||||||
} else if (transport > 0.18 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.04 || cityInfluence[i] > 0.09)) {
|
} else if (roadLanduseInfluence[i] > 0.30 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.05 || cityInfluence[i] > 0.11)) {
|
||||||
landuse[i] = transport > 0.28 && stationInfluence[i] > 0.10 ? LANDUSE.SUBURB : LANDUSE.ROADSIDE;
|
landuse[i] = LANDUSE.SUBURB;
|
||||||
} else if (agriculture[i] > 0.24 || rural > 0.22 || (developable[i] > 0.18 && plain[i] > 0.18)) {
|
} else if (agriculture[i] > 0.24 || rural > 0.22 || (developable[i] > 0.18 && plain[i] > 0.18)) {
|
||||||
landuse[i] = LANDUSE.FARMLAND;
|
landuse[i] = LANDUSE.FARMLAND;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -846,7 +827,7 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
for (let x = 1; x < MAP_W - 1; x++) {
|
for (let x = 1; x < MAP_W - 1; x++) {
|
||||||
const i = indexOf(x, y);
|
const i = indexOf(x, y);
|
||||||
if (sea[i] || baseLanduse[i] === LANDUSE.FOREST) continue;
|
if (sea[i] || baseLanduse[i] === LANDUSE.FOREST) continue;
|
||||||
const transport = Math.max(roadInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
||||||
let urbanNeighbors = 0;
|
let urbanNeighbors = 0;
|
||||||
let cbdNeighbors = 0;
|
let cbdNeighbors = 0;
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
|
@ -865,18 +846,12 @@ export function generateMapFeatures(seed, terrain) {
|
||||||
const fringeChance = urbanNeighbors * 0.055 + cityInfluence[i] * 0.13 + transport * 0.12 + stationInfluence[i] * 0.08;
|
const fringeChance = urbanNeighbors * 0.055 + cityInfluence[i] * 0.13 + transport * 0.12 + stationInfluence[i] * 0.08;
|
||||||
const noise = 0.23 + hash2(x, y, seed + 15050) * 0.24;
|
const noise = 0.23 + hash2(x, y, seed + 15050) * 0.24;
|
||||||
if (fringeChance > 0.34 + noise) {
|
if (fringeChance > 0.34 + noise) {
|
||||||
landuse[i] = urbanNeighbors >= 4 || transport > 0.28 ? LANDUSE.SUBURB : LANDUSE.ROADSIDE;
|
landuse[i] = LANDUSE.SUBURB;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadInfluence[i] > 0.22) && urbanNeighbors >= 3 && landuse[i] <= LANDUSE.FARMLAND && urbanCapacity[i] > 0.09) {
|
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadLanduseInfluence[i] > 0.22) && urbanNeighbors >= 3 && landuse[i] <= LANDUSE.FARMLAND && urbanCapacity[i] > 0.09) {
|
||||||
landuse[i] = cbdNeighbors >= 1 || coreInfluence[i] > 0.15 ? LANDUSE.OLD_URBAN : LANDUSE.SUBURB;
|
landuse[i] = cbdNeighbors >= 1 || coreInfluence[i] > 0.15 ? LANDUSE.OLD_URBAN : LANDUSE.SUBURB;
|
||||||
}
|
}
|
||||||
if (landuse[i] === LANDUSE.SUBURB && urbanNeighbors <= 1) {
|
|
||||||
const keep = clamp(cityInfluence[i] * 0.52 + transport * 0.32 + stationInfluence[i] * 0.18 + 0.08);
|
|
||||||
if (hash2(x, y, seed + 15051) > keep) {
|
|
||||||
landuse[i] = agriculture[i] > 0.22 ? LANDUSE.FARMLAND : LANDUSE.RURAL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { generateMap, CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapPipeline.js";
|
export { generateMap, generateMapAsync, CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapPipeline.js";
|
||||||
|
|
|
||||||
75
mapOutput.js
75
mapOutput.js
|
|
@ -1,6 +1,7 @@
|
||||||
import { createNameDebug } from "./names.js";
|
import { createNameDebug } from "./names.js";
|
||||||
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
|
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, hash2, indexOf, inside, rand } from "./mapUtils.js";
|
||||||
import { applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
|
import { applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
|
||||||
|
import { LANDUSE } from "./landuseCodes.js";
|
||||||
|
|
||||||
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
|
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
|
||||||
|
|
||||||
|
|
@ -24,6 +25,48 @@ function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
|
||||||
return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`;
|
return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureMunicipalOfficeSettlementFootprints(adminCentersRaw, { sea, elevation, slope, ridgeField, plain, basinField, coastalLowland, populationDensity, landuse, humanRegionMask, roadInfluence, stationInfluence }, seed) {
|
||||||
|
let changedCells = 0;
|
||||||
|
const isBuildable = (i) => !sea[i] && (!humanRegionMask || humanRegionMask[i]) && elevation[i] < 0.78 && slope[i] < 0.62 && ridgeField[i] < 0.76;
|
||||||
|
for (const [n, center] of (adminCentersRaw || []).entries()) {
|
||||||
|
if (!center || !inside(center.x, center.y)) continue;
|
||||||
|
const ci = indexOf(center.x, center.y);
|
||||||
|
if (!isBuildable(ci)) continue;
|
||||||
|
const existingUrban = landuse[ci] >= LANDUSE.OLD_URBAN && landuse[ci] <= LANDUSE.NEW_TOWN;
|
||||||
|
const baseRadius = existingUrban ? 1.2 : (center.population || 0) >= 60000 ? 2.6 : 2.0;
|
||||||
|
const scoreBoost = clamp((populationDensity[ci] || 0) * 0.45 + (roadInfluence?.[ci] || 0) * 0.22 + (stationInfluence?.[ci] || 0) * 0.26 + 0.24);
|
||||||
|
const r = Math.ceil(baseRadius);
|
||||||
|
for (let dy = -r; dy <= r; dy++) {
|
||||||
|
for (let dx = -r; dx <= r; dx++) {
|
||||||
|
const x = center.x + dx;
|
||||||
|
const y = center.y + dy;
|
||||||
|
if (!inside(x, y)) continue;
|
||||||
|
const i = indexOf(x, y);
|
||||||
|
if (!isBuildable(i)) continue;
|
||||||
|
const d = Math.hypot(dx, dy);
|
||||||
|
if (d > baseRadius) continue;
|
||||||
|
const lowland = clamp(plain[i] * 0.28 + basinField[i] * 0.24 + coastalLowland[i] * 0.20 + (roadInfluence?.[i] || 0) * 0.14 - slope[i] * 0.28 - ridgeField[i] * 0.16 + 0.30);
|
||||||
|
if (lowland <= 0.12) continue;
|
||||||
|
if (d <= 0.85) {
|
||||||
|
if (landuse[i] < LANDUSE.OLD_URBAN || landuse[i] === LANDUSE.FARMLAND || landuse[i] === LANDUSE.RURAL) {
|
||||||
|
landuse[i] = LANDUSE.OLD_URBAN;
|
||||||
|
changedCells++;
|
||||||
|
}
|
||||||
|
populationDensity[i] = Math.max(populationDensity[i] || 0, 0.34 + scoreBoost * 0.35);
|
||||||
|
} else if (d <= baseRadius && landuse[i] <= LANDUSE.FARMLAND) {
|
||||||
|
const keep = lowland * (1 - d / (baseRadius + 0.1)) + hash2(x, y, seed + 18800 + n) * 0.08;
|
||||||
|
if (keep > 0.16) {
|
||||||
|
landuse[i] = LANDUSE.SUBURB;
|
||||||
|
changedCells++;
|
||||||
|
populationDensity[i] = Math.max(populationDensity[i] || 0, 0.20 + scoreBoost * 0.20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedCells;
|
||||||
|
}
|
||||||
|
|
||||||
export function finishMapOutput({
|
export function finishMapOutput({
|
||||||
seed,
|
seed,
|
||||||
options,
|
options,
|
||||||
|
|
@ -141,9 +184,11 @@ export function finishMapOutput({
|
||||||
for (let i = 0; i < humanRegionMask.length; i++) {
|
for (let i = 0; i < humanRegionMask.length; i++) {
|
||||||
humanRegionMask[i] = !sea[i] && ((prefectureRegionId?.[i] ?? -1) >= 0 || prefectureMask[i]) ? 1 : 0;
|
humanRegionMask[i] = !sea[i] && ((prefectureRegionId?.[i] ?? -1) >= 0 || prefectureMask[i]) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
const municipalOfficeUrbanizedCells = ensureMunicipalOfficeSettlementFootprints(adminCentersRaw, {
|
||||||
|
sea, elevation, slope, ridgeField, plain, basinField, coastalLowland, populationDensity, landuse, humanRegionMask, roadInfluence, stationInfluence,
|
||||||
|
}, seed);
|
||||||
recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, humanRegionMask, sea, stationInfluence, roadInfluence, railInfluence2);
|
recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, humanRegionMask, sea, stationInfluence, roadInfluence, railInfluence2);
|
||||||
for (const city of modernCities) {
|
for (const city of modernCities) {
|
||||||
if (city.isPrefecturalCapital) continue;
|
|
||||||
const cap = cityPopulationCap(city);
|
const cap = cityPopulationCap(city);
|
||||||
if (cap < INF && (city.population || 0) > cap) {
|
if (cap < INF && (city.population || 0) > cap) {
|
||||||
city.population = Math.round(cap / 1000) * 1000;
|
city.population = Math.round(cap / 1000) * 1000;
|
||||||
|
|
@ -153,29 +198,6 @@ export function finishMapOutput({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputProgress("harbor works");
|
|
||||||
|
|
||||||
function makeHarborWorks(ports) {
|
|
||||||
const out = [];
|
|
||||||
for (const port of ports) {
|
|
||||||
const parts = [];
|
|
||||||
const limit = port.portClass === "major" ? 5 : port.portClass === "regional" ? 3 : 1;
|
|
||||||
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]) {
|
|
||||||
const sx = port.x + dx;
|
|
||||||
const sy = port.y + dy;
|
|
||||||
if (!inside(sx, sy) || !sea[indexOf(sx, sy)]) continue;
|
|
||||||
parts.push([[port.x, port.y], [sx, sy]]);
|
|
||||||
const wx = sx + dx;
|
|
||||||
const wy = sy + dy;
|
|
||||||
if (port.portClass === "major" && inside(wx, wy) && sea[indexOf(wx, wy)] && rand(seed, sx * 101 + sy * 103) > 0.22) parts.push([[sx, sy], [wx, wy]]);
|
|
||||||
if (parts.length >= limit) break;
|
|
||||||
}
|
|
||||||
if (parts.length) out.push({ port, segments: parts, kind: port.portClass === "major" ? "Major Harbor Works" : "Harbor Works" });
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
const harborWorks = makeHarborWorks(ports);
|
|
||||||
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
|
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
|
||||||
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity };
|
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity };
|
||||||
const usedNames = new Set();
|
const usedNames = new Set();
|
||||||
|
|
@ -340,12 +362,11 @@ export function finishMapOutput({
|
||||||
logisticsParks,
|
logisticsParks,
|
||||||
satelliteCities,
|
satelliteCities,
|
||||||
newTowns,
|
newTowns,
|
||||||
harborWorks,
|
|
||||||
landuse,
|
landuse,
|
||||||
adminCenters,
|
adminCenters,
|
||||||
adminId,
|
adminId,
|
||||||
adminBorders,
|
adminBorders,
|
||||||
adminDebug,
|
adminDebug: adminDebug ? { ...adminDebug, municipalOfficeUrbanizedCells } : { municipalOfficeUrbanizedCells },
|
||||||
castleRuins,
|
castleRuins,
|
||||||
riverPaths,
|
riverPaths,
|
||||||
mainRivers,
|
mainRivers,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ function nowMs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function timedStage(timings, options, key, label, fn) {
|
function timedStage(timings, options, key, label, fn) {
|
||||||
options?.onProgress?.({ status: "start", key, label, timings: timings.slice() });
|
options?.onProgress?.({ status: "start", key, label, timings: timings.slice(), startedAt: nowMs() });
|
||||||
const t0 = nowMs();
|
const t0 = nowMs();
|
||||||
const value = fn();
|
const value = fn();
|
||||||
const ms = Math.round((nowMs() - t0) * 10) / 10;
|
const ms = Math.round((nowMs() - t0) * 10) / 10;
|
||||||
|
|
@ -21,6 +21,27 @@ function timedStage(timings, options, key, label, fn) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function yieldToBrowser() {
|
||||||
|
if (typeof requestAnimationFrame === "function") {
|
||||||
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function timedStageAsync(timings, options, key, label, fn) {
|
||||||
|
options?.onProgress?.({ status: "start", key, label, timings: timings.slice(), startedAt: nowMs() });
|
||||||
|
await yieldToBrowser();
|
||||||
|
const t0 = nowMs();
|
||||||
|
const value = fn();
|
||||||
|
const ms = Math.round((nowMs() - t0) * 10) / 10;
|
||||||
|
const entry = { key, label, ms };
|
||||||
|
timings.push(entry);
|
||||||
|
options?.onProgress?.({ status: "done", key, label, ms, timings: timings.slice() });
|
||||||
|
await yieldToBrowser();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function generateMap(seedInput = 114514, options = {}) {
|
export function generateMap(seedInput = 114514, options = {}) {
|
||||||
const seed = Number(seedInput) >>> 0;
|
const seed = Number(seedInput) >>> 0;
|
||||||
if (typeof options.onProgress !== "function") options = { ...options, onProgress: () => {} };
|
if (typeof options.onProgress !== "function") options = { ...options, onProgress: () => {} };
|
||||||
|
|
@ -93,3 +114,76 @@ export function generateMap(seedInput = 114514, options = {}) {
|
||||||
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMapAsync(seedInput = 114514, options = {}) {
|
||||||
|
const seed = Number(seedInput) >>> 0;
|
||||||
|
if (typeof options.onProgress !== "function") options = { ...options, onProgress: () => {} };
|
||||||
|
|
||||||
|
const generationTimings = [];
|
||||||
|
const stage = (key, label, fn) => timedStageAsync(generationTimings, options, key, label, fn);
|
||||||
|
|
||||||
|
const terrain = await stage("terrain", "Terrain, rivers, and prefecture regions", () => generateTerrainAndRivers(seed));
|
||||||
|
const {
|
||||||
|
elevation,
|
||||||
|
slope,
|
||||||
|
sea,
|
||||||
|
river,
|
||||||
|
plain,
|
||||||
|
agriculture,
|
||||||
|
ridgeField,
|
||||||
|
valleyField,
|
||||||
|
basinField,
|
||||||
|
coastalLowland,
|
||||||
|
flowAccum,
|
||||||
|
naturalBarrierScore,
|
||||||
|
prefectureMask,
|
||||||
|
prefectureRegionId,
|
||||||
|
adminPrefectureRegionId,
|
||||||
|
} = terrain;
|
||||||
|
|
||||||
|
const features = await stage("human", "Human geography and transport", () => generateMapFeatures(seed, terrain));
|
||||||
|
const {
|
||||||
|
settlementScore,
|
||||||
|
villages,
|
||||||
|
markets,
|
||||||
|
modernCities,
|
||||||
|
populationDensity,
|
||||||
|
stations,
|
||||||
|
industrialZones,
|
||||||
|
logisticsParks,
|
||||||
|
satelliteCities,
|
||||||
|
newTowns,
|
||||||
|
ports,
|
||||||
|
landuse,
|
||||||
|
stationInfluence,
|
||||||
|
roadInfluence,
|
||||||
|
railInfluence2,
|
||||||
|
villageInfluence,
|
||||||
|
} = features;
|
||||||
|
|
||||||
|
const admin = await stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
|
||||||
|
seed, prefectureMask, prefectureRegionId: adminPrefectureRegionId || prefectureRegionId, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture,
|
||||||
|
settlementScore, populationDensity, stationInfluence, roadInfluence, railInfluence2, villageInfluence, landuse, modernCities, satelliteCities, newTowns, markets, villages, ports, stations, industrialZones, logisticsParks,
|
||||||
|
adminProgress: (event) => options?.onProgress?.({
|
||||||
|
...event,
|
||||||
|
key: "admin",
|
||||||
|
label: event.status === "region-done"
|
||||||
|
? `Admin region ${event.regionId} done`
|
||||||
|
: event.status === "admin-step"
|
||||||
|
? `Admin region ${event.regionId}: ${event.step}`
|
||||||
|
: `Admin region ${event.regionId}`,
|
||||||
|
timings: generationTimings.slice(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const output = await stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
|
||||||
|
seed,
|
||||||
|
options,
|
||||||
|
terrain,
|
||||||
|
features,
|
||||||
|
admin,
|
||||||
|
}));
|
||||||
|
output.generationTimings = generationTimings;
|
||||||
|
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
|
||||||
25
renderer.js
25
renderer.js
|
|
@ -313,15 +313,6 @@ function terrainColorContinuous(map, fx, fy, mode) {
|
||||||
if (isWaterSample(map, fx, fy)) {
|
if (isWaterSample(map, fx, fy)) {
|
||||||
const depth = clamp(((map.seaLevel ?? 0.285) + 0.065 - fieldSample(map.elevation, fx, fy)) * 2.4);
|
const depth = clamp(((map.seaLevel ?? 0.285) + 0.065 - fieldSample(map.elevation, fx, fy)) * 2.4);
|
||||||
color = [Math.round(170 + depth * 5), Math.round(218 + depth * 10), Math.round(255 - depth * 5)];
|
color = [Math.round(170 + depth * 5), Math.round(218 + depth * 10), Math.round(255 - depth * 5)];
|
||||||
} else if (mode === "suitability") {
|
|
||||||
const a = fieldSample(map.agriculture, fx, fy);
|
|
||||||
const p = fieldSample(map.plain, fx, fy);
|
|
||||||
const f = fieldSample(map.floodplain, fx, fy);
|
|
||||||
color = [
|
|
||||||
Math.round(240 - p * 15 + f * 10),
|
|
||||||
Math.round(242 + a * 10),
|
|
||||||
Math.round(235 - a * 15 + p * 10),
|
|
||||||
];
|
|
||||||
} else if (mode === "development") {
|
} else if (mode === "development") {
|
||||||
const dCity = distToNearest(map.modernCities, fx, fy);
|
const dCity = distToNearest(map.modernCities, fx, fy);
|
||||||
const urban = clamp(1 - dCity / 25);
|
const urban = clamp(1 - dCity / 25);
|
||||||
|
|
@ -436,7 +427,7 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
||||||
const width = MAP_W * CELL_SIZE;
|
const width = MAP_W * CELL_SIZE;
|
||||||
const height = MAP_H * CELL_SIZE;
|
const height = MAP_H * CELL_SIZE;
|
||||||
const img = ctx.createImageData(width, height);
|
const img = ctx.createImageData(width, height);
|
||||||
const continuousModes = ["terrain", "suitability", "development", "all"];
|
const continuousModes = ["terrain", "development", "all"];
|
||||||
|
|
||||||
if (continuousTerrain && continuousModes.includes(mode)) {
|
if (continuousTerrain && continuousModes.includes(mode)) {
|
||||||
for (let py = 0; py < height; py++) {
|
for (let py = 0; py < height; py++) {
|
||||||
|
|
@ -576,15 +567,17 @@ function drawUrbanAreas(ctx, map, mode) {
|
||||||
const visibleModes = ["all", "modern", "development", "landuse", "admin"];
|
const visibleModes = ["all", "modern", "development", "landuse", "admin"];
|
||||||
if (!visibleModes.includes(mode)) return;
|
if (!visibleModes.includes(mode)) return;
|
||||||
|
|
||||||
const colors = {
|
const detailedColors = {
|
||||||
2: "rgba(223, 214, 206, 0.72)",
|
2: "rgba(223, 214, 206, 0.72)",
|
||||||
3: "rgba(215, 175, 172, 0.88)",
|
3: "rgba(215, 175, 172, 0.88)",
|
||||||
4: "rgba(231, 219, 231, 0.68)",
|
4: "rgba(231, 219, 231, 0.68)",
|
||||||
5: "rgba(218, 218, 226, 0.64)",
|
5: "rgba(218, 218, 226, 0.64)",
|
||||||
6: "rgba(225, 230, 225, 0.56)",
|
6: "rgba(225, 230, 225, 0.56)",
|
||||||
7: "rgba(229, 234, 242, 0.64)",
|
7: "rgba(229, 234, 242, 0.64)",
|
||||||
8: "rgba(244, 230, 205, 0.62)",
|
8: "rgba(231, 219, 231, 0.68)",
|
||||||
};
|
};
|
||||||
|
const cityColor = "rgba(232, 222, 228, 0.60)";
|
||||||
|
const cbdColor = "rgba(215, 175, 172, 0.84)";
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
for (let y = 0; y < MAP_H; y++) {
|
for (let y = 0; y < MAP_H; y++) {
|
||||||
|
|
@ -593,11 +586,15 @@ function drawUrbanAreas(ctx, map, mode) {
|
||||||
const areaMask = map.humanRegionMask || map.prefectureMask;
|
const areaMask = map.humanRegionMask || map.prefectureMask;
|
||||||
if (areaMask && !areaMask[i]) continue;
|
if (areaMask && !areaMask[i]) continue;
|
||||||
const lu = map.landuse[i];
|
const lu = map.landuse[i];
|
||||||
if (!colors[lu]) continue;
|
let fill = null;
|
||||||
|
if (mode === "landuse") fill = detailedColors[lu] || null;
|
||||||
|
else if (lu === 3) fill = cbdColor;
|
||||||
|
else if (lu === 2 || lu === 4 || lu === 5 || lu === 6 || lu === 7 || lu === 8) fill = cityColor;
|
||||||
|
if (!fill) continue;
|
||||||
|
|
||||||
const px = x * CELL_SIZE;
|
const px = x * CELL_SIZE;
|
||||||
const py = y * CELL_SIZE;
|
const py = y * CELL_SIZE;
|
||||||
ctx.fillStyle = colors[lu];
|
ctx.fillStyle = fill;
|
||||||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,6 @@ code{background:#e8e8e8;border-radius:4px;padding:1px 4px}
|
||||||
.stat-row strong{color:#202124;font-family:ui-monospace,monospace}
|
.stat-row strong{color:#202124;font-family:ui-monospace,monospace}
|
||||||
.legend{color:#5f6368;font-size:13px;line-height:1.6}
|
.legend{color:#5f6368;font-size:13px;line-height:1.6}
|
||||||
.legend p{margin:8px 0 0}
|
.legend p{margin:8px 0 0}
|
||||||
.example{margin:10px 0;padding:12px;background:#f8f9fa;border:1px solid rgba(0,0,0,0.08);border-radius:8px;color:#3c4043;overflow:auto;font-size:12px}
|
|
||||||
.id-list{max-height:220px;overflow:auto;margin-top:12px;display:flex;flex-direction:column;gap:6px}
|
|
||||||
.id-row{display:grid;grid-template-columns:112px 1fr;gap:8px;align-items:center;color:#5f6368;font-size:12px}
|
|
||||||
|
|
||||||
@media (max-width:1100px){.layout{grid-template-columns:1fr}}
|
@media (max-width:1100px){.layout{grid-template-columns:1fr}}
|
||||||
|
|
||||||
|
|
@ -62,7 +59,6 @@ code{background:#e8e8e8;border-radius:4px;padding:1px 4px}
|
||||||
|
|
||||||
.port-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #4682c8;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
.port-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #4682c8;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
||||||
.newtown-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #b4d2f0;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
.newtown-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #b4d2f0;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
||||||
.legend-line.harbor-line{background:transparent; border-top:2px solid #5f7896; height:0}
|
|
||||||
|
|
||||||
.map-tooltip{position:absolute;z-index:20;pointer-events:none;min-width:200px;max-width:280px;background:rgba(255,255,255,0.98);border:1px solid rgba(0,0,0,0.1);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.1);padding:10px 12px;color:#2c2c2c;font-size:12px;line-height:1.5;opacity:0;transform:translateY(4px);transition:opacity 0.15s ease, transform 0.15s ease;font-weight:500}
|
.map-tooltip{position:absolute;z-index:20;pointer-events:none;min-width:200px;max-width:280px;background:rgba(255,255,255,0.98);border:1px solid rgba(0,0,0,0.1);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.1);padding:10px 12px;color:#2c2c2c;font-size:12px;line-height:1.5;opacity:0;transform:translateY(4px);transition:opacity 0.15s ease, transform 0.15s ease;font-weight:500}
|
||||||
.map-tooltip.visible{opacity:1;transform:translateY(0)}
|
.map-tooltip.visible{opacity:1;transform:translateY(0)}
|
||||||
|
|
|
||||||
2
test.js
2
test.js
|
|
@ -531,7 +531,6 @@ try {
|
||||||
assert(map.terrainDebug.depositionLowlandArea > 0, "depositional lowland area is tracked");
|
assert(map.terrainDebug.depositionLowlandArea > 0, "depositional lowland area is tracked");
|
||||||
assert(["east-west", "north-south", "diagonal"].includes(map.terrainTemplate.coastAxis) && map.terrainTemplate.coastSides?.length === 2, "paired coast template parameters are exposed");
|
assert(["east-west", "north-south", "diagonal"].includes(map.terrainTemplate.coastAxis) && map.terrainTemplate.coastSides?.length === 2, "paired coast template parameters are exposed");
|
||||||
assert(map.settlementCluster.length === size, "settlement cluster field matches map size");
|
assert(map.settlementCluster.length === size, "settlement cluster field matches map size");
|
||||||
assert(Array.isArray(map.harborWorks), "harbor arrays exist");
|
|
||||||
assert(map.prefectureRegionId.length === size && Array.isArray(map.regionalPrefectureBorders), "neighbor prefecture regions exist");
|
assert(map.prefectureRegionId.length === size && Array.isArray(map.regionalPrefectureBorders), "neighbor prefecture regions exist");
|
||||||
assert(map.regionalDebug && Number.isFinite(map.regionalDebug.regionalChangedAfterNaturalPartition), "regional changed-cell debug exists");
|
assert(map.regionalDebug && Number.isFinite(map.regionalDebug.regionalChangedAfterNaturalPartition), "regional changed-cell debug exists");
|
||||||
assert(map.regionalDebug.regionalChangedAfterNaturalPartition > 0, "regional natural partition changes region cells");
|
assert(map.regionalDebug.regionalChangedAfterNaturalPartition > 0, "regional natural partition changes region cells");
|
||||||
|
|
@ -592,7 +591,6 @@ try {
|
||||||
assert(terrainMetrics.depositionTargetMean >= terrainMetrics.depositionOtherMean * 0.85, "deposition favors rivers, basins, and coastal lowlands");
|
assert(terrainMetrics.depositionTargetMean >= terrainMetrics.depositionOtherMean * 0.85, "deposition favors rivers, basins, and coastal lowlands");
|
||||||
assert(terrainMetrics.riverValleyMean > terrainMetrics.nonRiverValleyMean * 1.08, "river cells overlap valley fields more than random non-river cells");
|
assert(terrainMetrics.riverValleyMean > terrainMetrics.nonRiverValleyMean * 1.08, "river cells overlap valley fields more than random non-river cells");
|
||||||
assert(terrainMetrics.alluvialMax > 0 || terrainMetrics.deltaMax > 0, "alluvial fan or delta fields are active");
|
assert(terrainMetrics.alluvialMax > 0 || terrainMetrics.deltaMax > 0, "alluvial fan or delta fields are active");
|
||||||
assert(map.harborWorks.length <= map.ports.length, "harbor works are attached to ports");
|
|
||||||
assert(map.ports.some((p) => p.portClass === "major"), "at least one major port is classified");
|
assert(map.ports.some((p) => p.portClass === "major"), "at least one major port is classified");
|
||||||
assert(map.ports.every((p) => ["major", "regional", "fishing", "lake"].includes(p.portClass)), "ports have explicit classes");
|
assert(map.ports.every((p) => ["major", "regional", "fishing", "lake"].includes(p.portClass)), "ports have explicit classes");
|
||||||
assert(map.externalGateways.length > 0, "external gateways exist");
|
assert(map.externalGateways.length > 0, "external gateways exist");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue