stable
This commit is contained in:
parent
860471f805
commit
c523bf4380
8 changed files with 76 additions and 26 deletions
7
app.js
7
app.js
|
|
@ -16,6 +16,7 @@ const modes = [
|
|||
|
||||
const state = {
|
||||
seedText: "114514",
|
||||
generationType: "auto",
|
||||
mode: "all",
|
||||
showFeatures: true,
|
||||
showLabels: true,
|
||||
|
|
@ -26,6 +27,7 @@ const state = {
|
|||
const canvas = document.getElementById("mapCanvas");
|
||||
const canvasShell = document.querySelector(".canvas-shell");
|
||||
const seedInput = document.getElementById("seed");
|
||||
const generationTypeInput = document.getElementById("generationType");
|
||||
const randomSeedButton = document.getElementById("randomSeed");
|
||||
const showFeaturesInput = document.getElementById("showFeatures");
|
||||
const showLabelsInput = document.getElementById("showLabels");
|
||||
|
|
@ -336,10 +338,11 @@ function renderModeButtons() {
|
|||
|
||||
async function regenerate() {
|
||||
state.seedText = seedInput.value;
|
||||
state.generationType = generationTypeInput?.value || "auto";
|
||||
setProgressVisible(true, "Preparing generation...");
|
||||
await nextFrame();
|
||||
try {
|
||||
state.map = await generateMapAsync(parseSeed(state.seedText), { onProgress: updateGenerationProgress });
|
||||
state.map = await generateMapAsync(parseSeed(state.seedText), { onProgress: updateGenerationProgress, terrainType: state.generationType });
|
||||
state.hoverEntities = buildHoverEntities(state.map);
|
||||
renderStats(state.map);
|
||||
redraw();
|
||||
|
|
@ -369,6 +372,8 @@ function init() {
|
|||
if (event.key === "Enter") regenerate();
|
||||
});
|
||||
|
||||
generationTypeInput?.addEventListener("change", regenerate);
|
||||
|
||||
randomSeedButton.addEventListener("click", () => {
|
||||
seedInput.value = String(Math.floor(Math.random() * 9999999));
|
||||
regenerate();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@
|
|||
<section class="card">
|
||||
<label class="label" for="seed">Seed</label>
|
||||
<input id="seed" class="input" value="114514" />
|
||||
<label class="label inline-label" for="generationType">Generation Type</label>
|
||||
<select id="generationType" class="input">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="tohoku_spine">Tohoku spine</option>
|
||||
<option value="chubu_mountain">Chubu mountain</option>
|
||||
<option value="setouchi_inland_sea">Setouchi inland sea</option>
|
||||
<option value="kanto_alluvial">Kanto alluvial plain</option>
|
||||
<option value="mixed_archipelago">Mixed archipelago</option>
|
||||
</select>
|
||||
<button id="randomSeed" type="button" class="primary-button">Generate Random Seed</button>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
39
mapOutput.js
39
mapOutput.js
|
|
@ -96,7 +96,7 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
|
|||
}
|
||||
|
||||
|
||||
function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, fields, seed, minPopulation = 200000) {
|
||||
function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, fields, seed, minPopulation = 200000, focusedPrefectureMask = null) {
|
||||
if (!prefectureRegionId) return 0;
|
||||
const prefIds = new Set();
|
||||
for (let i = 0; i < prefectureRegionId.length; i++) {
|
||||
|
|
@ -135,6 +135,24 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
|
|||
if (!p || !inside(p.x, p.y)) return -1;
|
||||
return prefectureRegionId[indexOf(p.x, p.y)] ?? -1;
|
||||
}
|
||||
const focusedPrefCounts = new Map();
|
||||
if (focusedPrefectureMask) {
|
||||
for (let i = 0; i < focusedPrefectureMask.length; i++) {
|
||||
if (!focusedPrefectureMask[i] || sea[i]) continue;
|
||||
const prefId = prefectureRegionId[i] ?? -1;
|
||||
if (prefId >= 0) focusedPrefCounts.set(prefId, (focusedPrefCounts.get(prefId) || 0) + 1);
|
||||
}
|
||||
}
|
||||
const focusedPrefId = focusedPrefCounts.size
|
||||
? [...focusedPrefCounts.entries()].sort((a, b) => b[1] - a[1])[0][0]
|
||||
: 0;
|
||||
for (const city of modernCities || []) {
|
||||
if (city.isPrefecturalCapital) {
|
||||
city.isPrefecturalCapital = false;
|
||||
city.rank = city.isRegionalCapital ? "Regional Capital" : city.population >= 200000 ? "Regional City" : "Local City";
|
||||
city.kind = city.rank;
|
||||
}
|
||||
}
|
||||
for (const prefId of [...prefIds].sort((a, b) => a - b)) {
|
||||
const cities = (modernCities || []).filter((p) => prefAt(p) === prefId);
|
||||
let target = cities.slice().sort((a, b) =>
|
||||
|
|
@ -175,10 +193,10 @@ function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCiti
|
|||
target.population = promotedPopulation;
|
||||
promoted++;
|
||||
}
|
||||
target.isPrefecturalCapital = true;
|
||||
target.isRegionalCapital = true;
|
||||
target.rank = "Prefectural Capital";
|
||||
target.kind = "Prefectural Capital";
|
||||
target.isPrefecturalCapital = prefId === focusedPrefId;
|
||||
target.rank = target.isPrefecturalCapital ? "Prefectural Capital" : "Regional Capital";
|
||||
target.kind = target.rank;
|
||||
target.labelPriorityBase = Math.max(target.labelPriorityBase || 0, 1150);
|
||||
target.urbanRadius = clamp(6.2 + Math.sqrt(target.population) / 80, 9, 30);
|
||||
target.coreRadius = clamp(2.0 + Math.sqrt(target.population) / 390, 2.4, 6.4);
|
||||
|
|
@ -480,6 +498,17 @@ export function finishMapOutput({
|
|||
center.canonicalSettlementId = best.id;
|
||||
center.canonicalSettlementName = best.name;
|
||||
center.municipalityRootName = best.name;
|
||||
const bestAdmin = adminId?.[indexOf(best.x, best.y)];
|
||||
const canSnapOffice = inside(best.x, best.y) && !sea[indexOf(best.x, best.y)] && (
|
||||
centerAdmin == null || centerAdmin < 0 || bestAdmin == null || bestAdmin < 0 || bestAdmin === centerAdmin
|
||||
);
|
||||
if (canSnapOffice) {
|
||||
center.generatedOfficeX = center.generatedOfficeX ?? center.x;
|
||||
center.generatedOfficeY = center.generatedOfficeY ?? center.y;
|
||||
center.x = best.x;
|
||||
center.y = best.y;
|
||||
center.officeSnappedToSettlement = true;
|
||||
}
|
||||
} else {
|
||||
center.municipalityRootName = center.generatedMunicipalityName;
|
||||
}
|
||||
|
|
@ -513,7 +542,7 @@ export function finishMapOutput({
|
|||
center.municipalityName = candidate;
|
||||
usedAdminNames.add(center.name);
|
||||
}
|
||||
const promotedPrefectureCapitals = promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, nameFields, seed, 220000);
|
||||
const promotedPrefectureCapitals = promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, nameFields, seed, 220000, prefectureMask);
|
||||
assignMunicipalityPopulations(adminCenters, adminId, nameFields, [
|
||||
...modernCities,
|
||||
...markets,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
const generationTimings = [];
|
||||
const stage = (key, label, fn) => timedStage(generationTimings, options, key, label, fn);
|
||||
|
||||
const terrain = stage("terrain", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed));
|
||||
const terrain = stage("terrain", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed, options));
|
||||
const {
|
||||
elevation,
|
||||
slope,
|
||||
|
|
@ -134,7 +134,7 @@ export async function generateMapAsync(seedInput = 114514, options = {}) {
|
|||
const generationTimings = [];
|
||||
const stage = (key, label, fn) => timedStageAsync(generationTimings, options, key, label, fn);
|
||||
|
||||
const terrain = await stage("terrain", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed));
|
||||
const terrain = await stage("terrain", "Terrain, rivers, and natural compartments", () => generateTerrainAndRivers(seed, options));
|
||||
const {
|
||||
elevation,
|
||||
slope,
|
||||
|
|
|
|||
|
|
@ -168,13 +168,13 @@ const TERRAIN_TYPES = [
|
|||
mountainOffsetRange: [0.47, 0.53],
|
||||
baseHeightRange: [0.56, 0.82],
|
||||
primaryLengthRange: [0.76, 0.96],
|
||||
primaryWidthRange: [0.13, 0.22],
|
||||
systemCountRange: [12, 16],
|
||||
beltCountRange: [3, 4],
|
||||
angleSpread: 0.14,
|
||||
crossSpread: 0.38,
|
||||
primaryWidthRange: [0.18, 0.30],
|
||||
systemCountRange: [14, 18],
|
||||
beltCountRange: [4, 5],
|
||||
angleSpread: 0.18,
|
||||
crossSpread: 0.58,
|
||||
lengthScale: 1.22,
|
||||
widthScale: 0.92,
|
||||
widthScale: 1.16,
|
||||
heightScale: 0.86,
|
||||
coastStrength: 0.90,
|
||||
plainBiasRange: [0.16, 0.34],
|
||||
|
|
@ -283,7 +283,12 @@ const TERRAIN_TYPES = [
|
|||
},
|
||||
];
|
||||
|
||||
function pickTerrainType(seed) {
|
||||
function pickTerrainType(seed, requestedType = "auto") {
|
||||
if (requestedType && requestedType !== "auto") {
|
||||
const normalizedType = requestedType === "touhoku_spine" ? "tohoku_spine" : requestedType;
|
||||
const selected = TERRAIN_TYPES.find((type) => type.id === normalizedType);
|
||||
if (selected) return selected;
|
||||
}
|
||||
// Terrain type selection is intentionally uniform. Individual terrain
|
||||
// templates still contain their own parameter ranges, but there is no
|
||||
// terrain-type appearance weighting.
|
||||
|
|
@ -299,8 +304,8 @@ function rangeInt(seed, salt, [lo, hi]) {
|
|||
return Math.round(lo + rand(seed, salt) * (hi - lo));
|
||||
}
|
||||
|
||||
export function buildTerrainTemplate(seed) {
|
||||
const terrainType = pickTerrainType(seed);
|
||||
export function buildTerrainTemplate(seed, options = {}) {
|
||||
const terrainType = pickTerrainType(seed, options.terrainType || options.generationType || "auto");
|
||||
const mountainMode = terrainType.mountainMode === "mixed"
|
||||
? (rand(seed, 12) < 0.42 ? "range" : rand(seed, 13) < 0.72 ? "mixed" : "massif")
|
||||
: terrainType.mountainMode;
|
||||
|
|
@ -1132,7 +1137,7 @@ function enforceLandGradient(elevation, sea, seaLevel) {
|
|||
}
|
||||
}
|
||||
|
||||
export function generateTerrainAndRivers(seed) {
|
||||
export function generateTerrainAndRivers(seed, options = {}) {
|
||||
const fields = createMapFields();
|
||||
fields.visibleRavineField = new Float32Array(SIZE);
|
||||
fields.surfaceTextureField = new Float32Array(SIZE);
|
||||
|
|
@ -1145,7 +1150,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
passSuitability,
|
||||
} = fields;
|
||||
|
||||
const terrainTemplate = buildTerrainTemplate(seed);
|
||||
const terrainTemplate = buildTerrainTemplate(seed, options);
|
||||
const systems = buildMountainSystems(terrainTemplate, seed);
|
||||
const allRidges = systems.flatMap((system, id) => buildScratchRidges(system, seed, id));
|
||||
|
||||
|
|
|
|||
|
|
@ -1367,11 +1367,12 @@ export function buildDensityFlowRoadTransportSystem(ctx) {
|
|||
}
|
||||
return best ? { target: best, d: bestD } : null;
|
||||
}
|
||||
function stitchEndpoints(paths, mode, targets, maxAdds, radius) {
|
||||
function stitchEndpoints(paths, mode, targets, maxAdds, radius, probability = 1) {
|
||||
let added = 0;
|
||||
const endpoints = endpointListWithIds(paths);
|
||||
for (const ep of endpoints) {
|
||||
if (added >= maxAdds) break;
|
||||
if (probability < 1 && hash2(ep.x, ep.y, seed + 18331 + added * 17) > probability) continue;
|
||||
const near = nearestWithin(ep, targets, radius, true);
|
||||
if (near && addShortConnector(paths, ep, near.target, mode)) added++;
|
||||
}
|
||||
|
|
@ -1380,7 +1381,7 @@ export function buildDensityFlowRoadTransportSystem(ctx) {
|
|||
const expresswayCells = sampledCells(expressways, 1);
|
||||
const nationalCells = sampledCells([...nationalRoads, ...externalRoads], 1);
|
||||
const localCells = sampledCells(minorRoads, 1);
|
||||
debug.expressway += stitchEndpoints(expressways, "expressway", expresswayCells, 8, 34.0);
|
||||
debug.expressway += stitchEndpoints(expressways, "expressway", expresswayCells, 14, 46.0, 0.62);
|
||||
debug.national += stitchEndpoints(nationalRoads, "national", nationalCells, 48, 14.0);
|
||||
debug.localToNational += stitchEndpoints(minorRoads, "local", nationalCells, 240, 20.0);
|
||||
debug.local += stitchEndpoints(minorRoads, "local", localCells, 260, 16.0);
|
||||
|
|
|
|||
|
|
@ -1018,8 +1018,8 @@ export function drawMap(canvas, map, options) {
|
|||
for (const path of map.externalRailways) drawLandPath(ctx, map, path, "rgba(255, 255, 255, 0.78)", 3.6, false, 3);
|
||||
}
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawLandPath(ctx, map, path, "rgba(105, 125, 105, 0.78)", 4.6);
|
||||
for (const path of map.externalExpressways) drawLandPath(ctx, map, path, "rgba(105, 125, 105, 0.78)", 4.6);
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(105, 125, 105, 0.78)", 4.6);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(105, 125, 105, 0.78)", 4.6);
|
||||
}
|
||||
|
||||
// 5. Transport fills. Expressways remain above railways; railways sit above ordinary roads.
|
||||
|
|
@ -1037,8 +1037,8 @@ export function drawMap(canvas, map, options) {
|
|||
for (const path of map.externalRailways) drawLandRailway(ctx, map, path, "rgba(95, 95, 95, 1)", 1.55, 5.0, 7.0);
|
||||
}
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawLandPath(ctx, map, path, "rgba(135, 160, 135, 0.88)", 2.4);
|
||||
for (const path of map.externalExpressways) drawLandPath(ctx, map, path, "rgba(135, 160, 135, 0.88)", 2.4);
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(135, 160, 135, 0.88)", 2.4);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(135, 160, 135, 0.88)", 2.4);
|
||||
}
|
||||
|
||||
// 6. Icons & Labels
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
*{box-sizing:border-box}
|
||||
body{margin:0;background:#f0f2f5;color:#2c2c2c;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
button,input{font:inherit}
|
||||
button,input,select{font:inherit}
|
||||
code{background:#e8e8e8;border-radius:4px;padding:1px 4px}
|
||||
.app{min-height:100vh;padding:16px}
|
||||
.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:16px;max-width:1360px;margin:0 auto}
|
||||
|
|
@ -13,6 +13,7 @@ code{background:#e8e8e8;border-radius:4px;padding:1px 4px}
|
|||
.sidebar{display:flex;flex-direction:column;gap:12px}
|
||||
.card{padding:14px}
|
||||
.label,.card-title{display:block;margin-bottom:12px;color:#1a1a1a;font-size:14px;font-weight:600}
|
||||
.inline-label{margin-top:12px}
|
||||
.input{width:100%;border:1px solid rgba(0,0,0,0.15);background:#fff;color:#2c2c2c;border-radius:8px;padding:10px 12px;outline:none;transition:border-color 0.2s}
|
||||
.input:focus{border-color:#1a73e8;box-shadow:0 0 0 3px rgba(26,115,232,0.15)}
|
||||
.primary-button,.mode-button{border:0;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600;transition:background 0.2s}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue