diff --git a/app.js b/app.js index c881bb3..c5ff6b4 100644 --- a/app.js +++ b/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"], ]; diff --git a/mapFeaturesV2.js b/mapFeaturesV2.js index eecedf9..1ee2a14 100644 --- a/mapFeaturesV2.js +++ b/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(); diff --git a/mapOutput.js b/mapOutput.js index 342a3b8..7f6d50c 100644 --- a/mapOutput.js +++ b/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, diff --git a/mapPipeline.js b/mapPipeline.js index f4f30c6..70729d7 100644 --- a/mapPipeline.js +++ b/mapPipeline.js @@ -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); diff --git a/renderer.js b/renderer.js index d08051f..4648d13 100644 --- a/renderer.js +++ b/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); } diff --git a/test.js b/test.js index f560706..7487b6d 100644 --- a/test.js +++ b/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");