not bad city system
This commit is contained in:
parent
f5e7a1df1d
commit
15c1031d72
6 changed files with 329 additions and 195 deletions
2
app.js
2
app.js
|
|
@ -8,11 +8,9 @@ const modes = [
|
|||
["suitability", "Suitability"],
|
||||
["history", "Premodern"],
|
||||
["modern", "Modern"],
|
||||
["roads", "Roads"],
|
||||
["development", "Development"],
|
||||
["landuse", "Land Use"],
|
||||
["admin", "Municipal Borders"],
|
||||
["terrain-debug", "Terrain Debug"],
|
||||
["admin-debug", "Admin Debug"],
|
||||
["borders-debug", "Borders Debug"],
|
||||
];
|
||||
|
|
|
|||
166
mapFeaturesV2.js
166
mapFeaturesV2.js
|
|
@ -108,16 +108,18 @@ export function generateMapFeatures(seed, terrain) {
|
|||
floodplain[i] * 0.03
|
||||
);
|
||||
valleySettlement[i] = clamp(
|
||||
valleyField[i] * 0.50 +
|
||||
river[i] * 0.16 +
|
||||
confluence * 0.72 +
|
||||
depositional * 0.16 +
|
||||
basinField[i] * 0.10 +
|
||||
lowSlope * 0.10 -
|
||||
slope[i] * 0.58 -
|
||||
ridgeField[i] * 0.34 -
|
||||
spine * 0.18 -
|
||||
highPenalty * 0.72
|
||||
valleyField[i] * 0.52 +
|
||||
river[i] * 0.08 +
|
||||
confluence * 0.38 +
|
||||
depositional * 0.20 +
|
||||
basinField[i] * 0.16 +
|
||||
plain[i] * 0.08 +
|
||||
lowSlope * 0.12 -
|
||||
slope[i] * 0.54 -
|
||||
ridgeField[i] * 0.30 -
|
||||
spine * 0.16 -
|
||||
highPenalty * 0.70 -
|
||||
floodplain[i] * 0.10
|
||||
);
|
||||
coastalSettlement[i] = clamp(
|
||||
coastalLowland[i] * 0.50 +
|
||||
|
|
@ -333,20 +335,21 @@ export function generateMapFeatures(seed, terrain) {
|
|||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const featurePull = Math.max(
|
||||
distanceToNearest(ports, x, y) < 8 ? 0.12 : 0,
|
||||
distanceToNearest(crossings, x, y) < 6 ? 0.10 : 0,
|
||||
confluenceField[i] * 0.26
|
||||
distanceToNearest(ports, x, y) < 8 ? 0.10 : 0,
|
||||
distanceToNearest(crossings, x, y) < 6 ? 0.06 : 0,
|
||||
confluenceField[i] * 0.16
|
||||
);
|
||||
const valleyMouth = valleyField[i] > 0.22 && (plain[i] > 0.22 || basinField[i] > 0.18 || coastalLowland[i] > 0.18) ? 0.12 : 0;
|
||||
const valleyMouth = valleyField[i] > 0.22 && (plain[i] > 0.22 || basinField[i] > 0.18 || coastalLowland[i] > 0.18) ? 0.14 : 0;
|
||||
marketScore[i] = clamp(
|
||||
townSuitability[i] * 0.66 +
|
||||
villageInfluence[i] * 0.42 +
|
||||
townSuitability[i] * 0.62 +
|
||||
villageInfluence[i] * 0.38 +
|
||||
featurePull +
|
||||
valleyMouth +
|
||||
basinField[i] * 0.10 +
|
||||
plain[i] * 0.09 -
|
||||
slope[i] * 0.20 -
|
||||
ridgeField[i] * 0.10
|
||||
basinField[i] * 0.12 +
|
||||
plain[i] * 0.14 +
|
||||
coastalLowland[i] * 0.08 -
|
||||
slope[i] * 0.18 -
|
||||
ridgeField[i] * 0.08
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -446,7 +449,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
Math.log10(capacity + 1) * 0.72 +
|
||||
townSuitability[i] * 1.40 +
|
||||
developable[i] * 1.05 +
|
||||
confluenceField[i] * 0.50 +
|
||||
confluenceField[i] * 0.22 +
|
||||
(p.candidateKind === "port" ? 0.48 : 0) +
|
||||
(p.candidateKind === "castleTown" ? 0.22 : 0) +
|
||||
hash2(p.x, p.y, seed + 12000) * 0.16;
|
||||
|
|
@ -523,9 +526,10 @@ export function generateMapFeatures(seed, terrain) {
|
|||
city.isRegionalCapital = isRegionalCapital;
|
||||
city.rank = isPrefecturalCapital ? "Prefectural Capital" : isRegionalCapital ? "Regional Capital" : city.population >= 200000 ? "Regional City" : "Local City";
|
||||
city.kind = city.rank;
|
||||
city.urbanRadius = clamp(4.5 + Math.sqrt(city.population) / 95, 7, isPrefecturalCapital ? 32 : isRegionalCapital ? 27 : 22);
|
||||
city.urbanRadius = clamp(5.8 + Math.sqrt(city.population) / 72, 8, isPrefecturalCapital ? 40 : isRegionalCapital ? 32 : 24);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 380, 2.2, isPrefecturalCapital ? 8.5 : 6.5);
|
||||
city.urbanWeight = clamp(0.85 + Math.log10(Math.max(10000, city.population)) * 0.25, 1.0, 2.2);
|
||||
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.urbanWeight = clamp(0.95 + Math.log10(Math.max(10000, city.population)) * 0.29, 1.08, 2.5);
|
||||
}
|
||||
|
||||
function cityPopulationCap(city) {
|
||||
|
|
@ -715,7 +719,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
const oldTownInfluence = influenceFromPoints([...markets, ...castleTowns, ...ports], 7, (p) => p.kind === "Major Port" ? 1.2 : 0.9);
|
||||
const populationDensity = new Float32Array(SIZE);
|
||||
|
||||
function addKernel(grid, p, radius, weight, exponent = 1.7, terrainWeighted = true) {
|
||||
function addKernel(grid, p, radius, weight, exponent = 1.7, terrainWeighted = true, combine = "max") {
|
||||
const r = Math.ceil(radius);
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
|
|
@ -726,16 +730,18 @@ export function generateMapFeatures(seed, terrain) {
|
|||
if (sea[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius) continue;
|
||||
const terrain = terrainWeighted ? clamp(0.20 + developable[i] * 1.05 + valleySettlement[i] * 0.18 + coastalSettlement[i] * 0.10 - slope[i] * 0.22 - ridgeField[i] * 0.14, 0, 1.28) : 1;
|
||||
const terrain = terrainWeighted ? clamp(0.24 + developable[i] * 1.00 + valleySettlement[i] * 0.16 + coastalSettlement[i] * 0.10 - slope[i] * 0.20 - ridgeField[i] * 0.12 + roadInfluence[i] * 0.08 + railInfluence2[i] * 0.06, 0, 1.34) : 1;
|
||||
const v = weight * Math.pow(1 - d / Math.max(1, radius), exponent) * terrain;
|
||||
if (v > grid[i]) grid[i] = v;
|
||||
if (combine === "add") grid[i] = Math.min(3.4, grid[i] + v);
|
||||
else if (v > grid[i]) grid[i] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const city of modernCities) {
|
||||
addKernel(cityInfluence, city, city.urbanRadius || 10, city.urbanWeight || 1.0, 1.25, true);
|
||||
addKernel(coreInfluence, city, city.coreRadius || 3, (city.urbanWeight || 1.0) * 1.10, 1.65, true);
|
||||
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.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");
|
||||
}
|
||||
const townInfluence = influenceFromPoints(markets, 6, (m) => clamp((m.population || 10000) / 26000, 0.45, 1.25));
|
||||
|
||||
|
|
@ -774,26 +780,106 @@ export function generateMapFeatures(seed, terrain) {
|
|||
function classifyLanduse() {
|
||||
landuse.fill(LANDUSE.RURAL);
|
||||
let maxDensity = 0;
|
||||
const baseNoiseSeed = seed + 15000;
|
||||
const urbanCapacity = new Float32Array(SIZE);
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const urban = cityInfluence[i] * 0.78 + stationInfluence[i] * 0.22 + roadInfluence[i] * 0.08;
|
||||
const transport = Math.max(roadInfluence[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 core = coreInfluence[i];
|
||||
const oldTown = oldTownInfluence[i] * 0.72 + townInfluence[i] * 0.42;
|
||||
const rural = villageInfluence[i] * 0.25 + ruralSuitability[i] * 0.30;
|
||||
populationDensity[i] = clamp(urban * 0.74 + core * 0.32 + oldTown * 0.28 + townInfluence[i] * 0.18 + villageInfluence[i] * 0.16 + roadInfluence[i] * 0.05);
|
||||
const oldTown = oldTownInfluence[i] * 0.70 + townInfluence[i] * 0.38;
|
||||
const rural = villageInfluence[i] * 0.24 + ruralSuitability[i] * 0.30;
|
||||
const riverUrban = clamp(river[i] * 0.12 + valleyField[i] * 0.10 + plain[i] * 0.08 + basinField[i] * 0.08 - floodplain[i] * 0.10);
|
||||
urbanCapacity[i] = clamp(
|
||||
developable[i] * 0.66 +
|
||||
plain[i] * 0.16 +
|
||||
basinField[i] * 0.16 +
|
||||
valleyField[i] * 0.16 +
|
||||
coastalLowland[i] * 0.12 +
|
||||
transport * 0.18 +
|
||||
riverUrban * 0.14 -
|
||||
slope[i] * 0.18 -
|
||||
ridgeField[i] * 0.12 -
|
||||
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);
|
||||
maxDensity = Math.max(maxDensity, populationDensity[i]);
|
||||
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) landuse[i] = LANDUSE.FOREST;
|
||||
else if (industrialInfluence[i] > 0.22 && developable[i] > 0.08) landuse[i] = LANDUSE.INDUSTRIAL;
|
||||
else if (core > 0.38 && developable[i] > 0.07) landuse[i] = LANDUSE.CBD;
|
||||
else if (oldTown > 0.24 && developable[i] > 0.06) landuse[i] = LANDUSE.OLD_URBAN;
|
||||
else if (urban > 0.22 && developable[i] > 0.08) landuse[i] = LANDUSE.SUBURB;
|
||||
else if (roadInfluence[i] > 0.22 && developable[i] > 0.16 && townInfluence[i] > 0.07) landuse[i] = LANDUSE.ROADSIDE;
|
||||
else if (agriculture[i] > 0.22 || rural > 0.22 || (developable[i] > 0.18 && plain[i] > 0.16)) landuse[i] = LANDUSE.FARMLAND;
|
||||
else landuse[i] = elevation[i] > 0.52 || slope[i] > 0.34 ? LANDUSE.FOREST : LANDUSE.RURAL;
|
||||
|
||||
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
|
||||
landuse[i] = LANDUSE.FOREST;
|
||||
continue;
|
||||
}
|
||||
if (industrialInfluence[i] > 0.22 && urbanCapacity[i] > 0.10) {
|
||||
landuse[i] = LANDUSE.INDUSTRIAL;
|
||||
continue;
|
||||
}
|
||||
if (core > 0.38 && urbanCapacity[i] > 0.10) {
|
||||
landuse[i] = LANDUSE.CBD;
|
||||
continue;
|
||||
}
|
||||
if (oldTown > 0.18 && urbanCapacity[i] > 0.09) {
|
||||
landuse[i] = LANDUSE.OLD_URBAN;
|
||||
continue;
|
||||
}
|
||||
|
||||
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 sprawlBias = clamp(0.58 + hash2(x, y, baseNoiseSeed) * 0.42);
|
||||
const sprawlScore = suburbanity * sprawlBias * edgeTaper - core * 0.12;
|
||||
if (sprawlScore > 0.24 && urbanCapacity[i] > 0.10) {
|
||||
landuse[i] = LANDUSE.SUBURB;
|
||||
} else if (transport > 0.18 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.04 || cityInfluence[i] > 0.09)) {
|
||||
landuse[i] = transport > 0.28 && stationInfluence[i] > 0.10 ? LANDUSE.SUBURB : LANDUSE.ROADSIDE;
|
||||
} else if (agriculture[i] > 0.24 || rural > 0.22 || (developable[i] > 0.18 && plain[i] > 0.18)) {
|
||||
landuse[i] = LANDUSE.FARMLAND;
|
||||
} else {
|
||||
landuse[i] = elevation[i] > 0.52 || slope[i] > 0.34 ? LANDUSE.FOREST : LANDUSE.RURAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const baseLanduse = landuse.slice();
|
||||
const isBuilt = (lu) => lu >= LANDUSE.OLD_URBAN && lu <= LANDUSE.ROADSIDE;
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || baseLanduse[i] === LANDUSE.FOREST) continue;
|
||||
const transport = Math.max(roadInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
||||
let urbanNeighbors = 0;
|
||||
let cbdNeighbors = 0;
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const lu = baseLanduse[indexOf(x + dx, y + dy)];
|
||||
if (isBuilt(lu)) urbanNeighbors++;
|
||||
if (lu === LANDUSE.CBD) cbdNeighbors++;
|
||||
}
|
||||
}
|
||||
if (baseLanduse[i] === LANDUSE.OLD_URBAN && coreInfluence[i] > 0.31 && cbdNeighbors >= 3) {
|
||||
landuse[i] = LANDUSE.CBD;
|
||||
continue;
|
||||
}
|
||||
if ((baseLanduse[i] === LANDUSE.FARMLAND || baseLanduse[i] === LANDUSE.RURAL) && urbanCapacity[i] > 0.10) {
|
||||
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;
|
||||
if (fringeChance > 0.34 + noise) {
|
||||
landuse[i] = urbanNeighbors >= 4 || transport > 0.28 ? LANDUSE.SUBURB : LANDUSE.ROADSIDE;
|
||||
}
|
||||
}
|
||||
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadInfluence[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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDensity > 0) for (let i = 0; i < SIZE; i++) populationDensity[i] = clamp(populationDensity[i] / maxDensity);
|
||||
}
|
||||
classifyLanduse();
|
||||
|
|
|
|||
192
mapOutput.js
192
mapOutput.js
|
|
@ -27,84 +27,112 @@ function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
|
|||
export function finishMapOutput({
|
||||
seed,
|
||||
options,
|
||||
terrainTemplate,
|
||||
seaLevel,
|
||||
cityPopulationCap,
|
||||
stationInfluence,
|
||||
roadInfluence,
|
||||
railInfluence2,
|
||||
elevation,
|
||||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
ocean,
|
||||
lake,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
agriculture,
|
||||
settlementCluster,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
visibleRavineField,
|
||||
surfaceTextureField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
arcSpineField,
|
||||
branchRidgeField,
|
||||
depositionalLowland,
|
||||
alluvialFanField,
|
||||
deltaField,
|
||||
naturalBarrierScore,
|
||||
villages,
|
||||
ports,
|
||||
crossings,
|
||||
passes,
|
||||
markets,
|
||||
castles,
|
||||
castleTowns,
|
||||
premodernRoads,
|
||||
minorRoads,
|
||||
modernCities,
|
||||
populationDensity,
|
||||
railways,
|
||||
branchRailways,
|
||||
ringRailways,
|
||||
externalRailways,
|
||||
stations,
|
||||
industrialZones,
|
||||
nationalRoads,
|
||||
ringRoads,
|
||||
expressways,
|
||||
ringExpressways,
|
||||
icAccessRoads,
|
||||
externalRoads,
|
||||
externalExpressways,
|
||||
interchanges,
|
||||
logisticsParks,
|
||||
satelliteCities,
|
||||
newTowns,
|
||||
landuse,
|
||||
adminCentersRaw,
|
||||
adminId,
|
||||
adminBorders,
|
||||
adminDebug,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
tributaryRivers,
|
||||
smallStreams,
|
||||
externalGateways,
|
||||
transportDebug,
|
||||
prefectureMask,
|
||||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
terrain,
|
||||
features,
|
||||
admin,
|
||||
}) {
|
||||
const {
|
||||
terrainTemplate,
|
||||
seaLevel,
|
||||
elevation,
|
||||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
ocean,
|
||||
lake,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
agriculture,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
visibleRavineField,
|
||||
surfaceTextureField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
arcSpineField,
|
||||
branchRidgeField,
|
||||
depositionalLowland,
|
||||
alluvialFanField,
|
||||
deltaField,
|
||||
naturalBarrierScore,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
tributaryRivers,
|
||||
smallStreams,
|
||||
prefectureMask,
|
||||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
} = terrain;
|
||||
|
||||
const {
|
||||
cityPopulationCap,
|
||||
stationInfluence,
|
||||
roadInfluence,
|
||||
railInfluence2,
|
||||
settlementCluster,
|
||||
villages: inputVillages,
|
||||
ports: inputPorts,
|
||||
crossings: inputCrossings,
|
||||
passes: inputPasses,
|
||||
markets: inputMarkets,
|
||||
castles: inputCastles,
|
||||
castleTowns: inputCastleTowns,
|
||||
premodernRoads,
|
||||
minorRoads,
|
||||
modernCities: inputModernCities,
|
||||
populationDensity,
|
||||
railways,
|
||||
branchRailways,
|
||||
ringRailways,
|
||||
externalRailways,
|
||||
stations: inputStations,
|
||||
industrialZones: inputIndustrialZones,
|
||||
nationalRoads,
|
||||
ringRoads,
|
||||
expressways,
|
||||
ringExpressways,
|
||||
icAccessRoads,
|
||||
externalRoads,
|
||||
externalExpressways,
|
||||
interchanges: inputInterchanges,
|
||||
logisticsParks: inputLogisticsParks,
|
||||
satelliteCities: inputSatelliteCities,
|
||||
newTowns: inputNewTowns,
|
||||
landuse,
|
||||
externalGateways: inputExternalGateways,
|
||||
transportDebug,
|
||||
} = features;
|
||||
|
||||
const {
|
||||
adminCentersRaw,
|
||||
adminId,
|
||||
adminBorders,
|
||||
adminDebug,
|
||||
} = admin;
|
||||
|
||||
let villages = inputVillages;
|
||||
let ports = inputPorts;
|
||||
let crossings = inputCrossings;
|
||||
let passes = inputPasses;
|
||||
let markets = inputMarkets;
|
||||
let castles = inputCastles;
|
||||
let castleTowns = inputCastleTowns;
|
||||
let modernCities = inputModernCities;
|
||||
let stations = inputStations;
|
||||
let industrialZones = inputIndustrialZones;
|
||||
let interchanges = inputInterchanges;
|
||||
let logisticsParks = inputLogisticsParks;
|
||||
let satelliteCities = inputSatelliteCities;
|
||||
let newTowns = inputNewTowns;
|
||||
let externalGateways = inputExternalGateways;
|
||||
|
||||
const outputProgress = (step) => options?.onProgress?.({ status: "output-step", key: "output", label: `Output: ${step}`, step });
|
||||
outputProgress("population recalculation");
|
||||
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
|
||||
|
|
@ -147,14 +175,8 @@ export function finishMapOutput({
|
|||
return out;
|
||||
}
|
||||
|
||||
// Bridge and tunnel icon systems were removed from the visual model.
|
||||
// Arrays remain empty for backward-compatible tests and downstream code.
|
||||
const bridges = [];
|
||||
const tunnels = [];
|
||||
const harborWorks = makeHarborWorks(ports);
|
||||
const abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
|
||||
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
|
||||
const preservedOldRoads = premodernRoads.filter((_, i) => i % 2 === 0);
|
||||
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity };
|
||||
const usedNames = new Set();
|
||||
const nameDebug = createNameDebug();
|
||||
|
|
@ -318,17 +340,13 @@ export function finishMapOutput({
|
|||
logisticsParks,
|
||||
satelliteCities,
|
||||
newTowns,
|
||||
bridges,
|
||||
tunnels,
|
||||
harborWorks,
|
||||
landuse,
|
||||
adminCenters,
|
||||
adminId,
|
||||
adminBorders,
|
||||
adminDebug,
|
||||
abandonedRailways,
|
||||
castleRuins,
|
||||
preservedOldRoads,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
tributaryRivers,
|
||||
|
|
|
|||
|
|
@ -30,57 +30,44 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
|
||||
const terrain = stage("terrain", "Terrain, rivers, and prefecture regions", () => generateTerrainAndRivers(seed));
|
||||
const {
|
||||
terrainTemplate,
|
||||
seaLevel,
|
||||
elevation,
|
||||
moisture,
|
||||
slope,
|
||||
sea,
|
||||
ocean,
|
||||
lake,
|
||||
river,
|
||||
floodplain,
|
||||
plain,
|
||||
agriculture,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
visibleRavineField,
|
||||
surfaceTextureField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
erosionField,
|
||||
depositionField,
|
||||
arcSpineField,
|
||||
branchRidgeField,
|
||||
depositionalLowland,
|
||||
alluvialFanField,
|
||||
deltaField,
|
||||
naturalBarrierScore,
|
||||
portSuitability,
|
||||
crossingSuitability,
|
||||
passSuitability,
|
||||
prefectureMask,
|
||||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
adminPrefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
tributaryRivers,
|
||||
smallStreams,
|
||||
} = terrain;
|
||||
|
||||
const features = stage("human", "Human geography and transport", () => generateMapFeatures(seed, terrain));
|
||||
const {
|
||||
ports, crossings, passes, settlementCluster, settlementScore, villages, markets, castles, premodernRoads, minorRoads, castleTowns, modernCities, populationDensity,
|
||||
railways, branchRailways, ringRailways, externalRailways, stations, industrialZones, nationalRoads, ringRoads, expressways, ringExpressways, icAccessRoads, externalRoads, externalExpressways,
|
||||
interchanges, logisticsParks, satelliteCities, newTowns, landuse, stationInfluence, roadInfluence, railInfluence2, villageInfluence, externalGateways, cityPopulationCap, transportDebug,
|
||||
settlementScore,
|
||||
villages,
|
||||
markets,
|
||||
modernCities,
|
||||
populationDensity,
|
||||
stations,
|
||||
industrialZones,
|
||||
logisticsParks,
|
||||
satelliteCities,
|
||||
newTowns,
|
||||
ports,
|
||||
landuse,
|
||||
stationInfluence,
|
||||
roadInfluence,
|
||||
railInfluence2,
|
||||
villageInfluence,
|
||||
} = features;
|
||||
|
||||
const { adminCentersRaw, adminId, adminBorders, adminDebug } = stage("admin", "Municipal and prefectural administration", () => generateAdminLayout({
|
||||
const admin = 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?.({
|
||||
|
|
@ -96,14 +83,11 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
}));
|
||||
|
||||
const output = stage("output", "Names, population totals, and final packaging", () => finishMapOutput({
|
||||
seed, options, terrainTemplate, seaLevel, cityPopulationCap, stationInfluence, roadInfluence, railInfluence2,
|
||||
elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, settlementCluster, ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField, coastalLowland, flowAccum, erosionField, depositionField,
|
||||
arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, deltaField, naturalBarrierScore,
|
||||
villages, ports, crossings, passes, markets, castles, castleTowns, premodernRoads, minorRoads, modernCities, populationDensity,
|
||||
railways, branchRailways, ringRailways, externalRailways, stations, industrialZones, nationalRoads, ringRoads, expressways, ringExpressways, icAccessRoads, externalRoads, externalExpressways,
|
||||
interchanges, logisticsParks, satelliteCities, newTowns, landuse, adminCentersRaw, adminId, adminBorders, adminDebug,
|
||||
riverPaths, mainRivers, tributaryRivers, smallStreams, externalGateways, prefectureMask, prefectureBorder, prefectureRegionId, regionalPrefectureBorders,
|
||||
regionalDebug, terrainDebug, transportDebug,
|
||||
seed,
|
||||
options,
|
||||
terrain,
|
||||
features,
|
||||
admin,
|
||||
}));
|
||||
output.generationTimings = generationTimings;
|
||||
output.generationTotalMs = generationTimings.reduce((sum, row) => sum + row.ms, 0);
|
||||
|
|
|
|||
102
renderer.js
102
renderer.js
|
|
@ -407,16 +407,16 @@ function discreteColor(map, x, y, mode) {
|
|||
color = [160, 205, 239];
|
||||
} else if (mode === "landuse") {
|
||||
const colors = {
|
||||
0: [242, 248, 238], // rural / natural land
|
||||
1: [238, 246, 222], // farmland
|
||||
2: [240, 238, 232], // old urban
|
||||
3: [245, 230, 220], // CBD / DID core
|
||||
4: [250, 248, 245], // suburb
|
||||
5: [235, 235, 240], // industrial
|
||||
6: [240, 245, 240], // logistics
|
||||
7: [245, 248, 252], // new town
|
||||
8: [250, 248, 240], // roadside
|
||||
9: [225, 238, 220], // forest / mountain land
|
||||
0: [244, 247, 240], // rural / natural land
|
||||
1: [222, 236, 188], // farmland
|
||||
2: [232, 222, 214], // old urban
|
||||
3: [221, 188, 184], // CBD / DID core
|
||||
4: [235, 225, 236], // suburb
|
||||
5: [223, 224, 232], // industrial
|
||||
6: [232, 237, 232], // logistics
|
||||
7: [231, 236, 246], // new town
|
||||
8: [243, 233, 210], // roadside
|
||||
9: [221, 236, 216], // forest / mountain land
|
||||
};
|
||||
color = colors[map.landuse[i]] || colors[0];
|
||||
} else if (mode === "admin") {
|
||||
|
|
@ -573,24 +573,25 @@ function drawSegments(ctx, segments, color, width, dashed = false) {
|
|||
}
|
||||
|
||||
function drawUrbanAreas(ctx, map, mode) {
|
||||
const visibleModes = ["all", "modern", "development", "landuse", "roads", "admin"];
|
||||
const visibleModes = ["all", "modern", "development", "landuse", "admin"];
|
||||
if (!visibleModes.includes(mode)) return;
|
||||
|
||||
const colors = {
|
||||
2: "rgba(225, 222, 215, 0.6)",
|
||||
3: "rgba(240, 220, 205, 0.85)",
|
||||
4: "rgba(242, 240, 235, 0.5)",
|
||||
5: "rgba(220, 220, 225, 0.6)",
|
||||
6: "rgba(225, 230, 225, 0.5)",
|
||||
7: "rgba(235, 240, 245, 0.6)",
|
||||
8: "rgba(245, 242, 235, 0.5)",
|
||||
2: "rgba(223, 214, 206, 0.72)",
|
||||
3: "rgba(215, 175, 172, 0.88)",
|
||||
4: "rgba(231, 219, 231, 0.68)",
|
||||
5: "rgba(218, 218, 226, 0.64)",
|
||||
6: "rgba(225, 230, 225, 0.56)",
|
||||
7: "rgba(229, 234, 242, 0.64)",
|
||||
8: "rgba(244, 230, 205, 0.62)",
|
||||
};
|
||||
|
||||
ctx.save();
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (!map.prefectureMask[i]) continue;
|
||||
const areaMask = map.humanRegionMask || map.prefectureMask;
|
||||
if (areaMask && !areaMask[i]) continue;
|
||||
const lu = map.landuse[i];
|
||||
if (!colors[lu]) continue;
|
||||
|
||||
|
|
@ -678,6 +679,42 @@ function drawLabels(ctx, points, limit = Infinity) {
|
|||
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
|
||||
}
|
||||
|
||||
function drawScaleBar(ctx) {
|
||||
const kmPerCell = 2;
|
||||
const targetKm = 20;
|
||||
const lengthCells = Math.max(8, Math.round(targetKm / kmPerCell));
|
||||
const lengthPx = lengthCells * CELL_SIZE;
|
||||
const margin = 14;
|
||||
const x = margin;
|
||||
const y = margin + 18;
|
||||
|
||||
ctx.save();
|
||||
ctx.lineCap = "butt";
|
||||
ctx.strokeStyle = "rgba(0,0,0,0.78)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.fillStyle = "rgba(255,255,255,0.92)";
|
||||
ctx.fillRect(x - 8, y - 18, lengthPx + 16, 30);
|
||||
ctx.strokeStyle = "rgba(80,80,80,0.22)";
|
||||
ctx.strokeRect(x - 8, y - 18, lengthPx + 16, 30);
|
||||
ctx.strokeStyle = "rgba(30,30,30,0.82)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + lengthPx, y);
|
||||
ctx.stroke();
|
||||
for (let t = 0; t <= 2; t++) {
|
||||
const tx = x + (lengthPx * t) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tx, y - 5);
|
||||
ctx.lineTo(tx, y + 5);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "rgba(20,20,20,0.88)";
|
||||
ctx.font = "600 11px ui-sans-serif, system-ui, -apple-system, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(`${targetKm} km`, x + lengthPx / 2, y - 7);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function drawMap(canvas, map, options) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
|
@ -749,9 +786,9 @@ export function drawMap(canvas, map, options) {
|
|||
}
|
||||
|
||||
const showHistory = ["history", "all", "terrain"].includes(mode);
|
||||
const showModern = ["modern", "all", "development", "landuse", "roads", "admin-debug", "borders-debug"].includes(mode);
|
||||
const showRoads = ["roads", "all", "development"].includes(mode);
|
||||
const showMinorRoads = ["roads", "all", "modern", "development"].includes(mode);
|
||||
const showModern = ["modern", "all", "development", "landuse", "admin-debug", "borders-debug"].includes(mode);
|
||||
const showRoads = ["all", "development"].includes(mode);
|
||||
const showMinorRoads = ["all", "modern", "development"].includes(mode);
|
||||
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
|
||||
|
||||
// 3. Borders
|
||||
|
|
@ -829,11 +866,15 @@ export function drawMap(canvas, map, options) {
|
|||
|
||||
if (showModern) {
|
||||
for (const p of map.stations) dot(ctx, p, 2.5, "#fff", "#444");
|
||||
const allLayerTowns = mode === "all"
|
||||
? (map.markets || []).filter((p) => (p.population || 0) >= 25000 && !(map.modernCities || []).some((c) => c.x === p.x && c.y === p.y))
|
||||
: [];
|
||||
for (const p of allLayerTowns) dot(ctx, p, 3.1, "rgba(244, 164, 118, 0.92)", "rgba(255,255,255,0.88)");
|
||||
for (const p of map.modernCities) {
|
||||
const popRadius = p.population ? Math.min(8.5, 3.5 + Math.sqrt(p.population) / 400) : 4.5;
|
||||
const popRadius = p.population ? Math.min(9.4, 3.8 + Math.sqrt(p.population) / 360) : 4.8;
|
||||
dot(ctx, p, popRadius, "rgba(240, 110, 110, 0.95)", "rgba(255,255,255,0.9)");
|
||||
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 3.0, "transparent", "rgba(200,80,80,0.9)");
|
||||
else if (p.isRegionalCapital) dot(ctx, p, popRadius + 2.2, "transparent", "rgba(190,95,95,0.62)");
|
||||
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 3.4, "transparent", "rgba(200,80,80,0.9)");
|
||||
else if (p.isRegionalCapital) dot(ctx, p, popRadius + 2.4, "transparent", "rgba(190,95,95,0.62)");
|
||||
}
|
||||
for (const p of map.interchanges || []) dot(ctx, p, 2.7, "rgba(250, 250, 245, 0.95)", "rgba(95, 125, 95, 0.95)");
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
|
|
@ -845,17 +886,24 @@ export function drawMap(canvas, map, options) {
|
|||
if (showLabels) {
|
||||
if (mode === "admin") {
|
||||
drawLabels(ctx, map.adminCenters || [], Infinity);
|
||||
drawScaleBar(ctx);
|
||||
return;
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
drawLabels(ctx, map.adminCenters || [], Infinity);
|
||||
drawScaleBar(ctx);
|
||||
return;
|
||||
}
|
||||
const allLayerTowns = mode === "all"
|
||||
? (map.markets || []).filter((p) => (p.population || 0) >= 25000 && !(map.modernCities || []).some((c) => c.x === p.x && c.y === p.y)).map((p) => ({ ...p, labelPriorityBase: 120 }))
|
||||
: [];
|
||||
const important = [
|
||||
...map.modernCities,
|
||||
...map.ports,
|
||||
...(map.satelliteCities || []),
|
||||
].filter((p) => p.insidePrefecture || p.isRegionalCapital || p.kind === "External Gateway");
|
||||
drawLabels(ctx, important, 60);
|
||||
...allLayerTowns,
|
||||
].filter((p) => p.insidePrefecture || p.isRegionalCapital || p.kind === "External Gateway" || (p.population || 0) >= 25000);
|
||||
drawLabels(ctx, important, mode === "all" ? 85 : 60);
|
||||
}
|
||||
drawScaleBar(ctx);
|
||||
}
|
||||
|
|
|
|||
2
test.js
2
test.js
|
|
@ -531,7 +531,7 @@ try {
|
|||
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(map.settlementCluster.length === size, "settlement cluster field matches map size");
|
||||
assert(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist");
|
||||
assert(Array.isArray(map.harborWorks), "harbor arrays 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.regionalChangedAfterNaturalPartition > 0, "regional natural partition changes region cells");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue