import { MAP_H, MAP_W, SIZE, clamp, hash2, indexOf, inside } from "./mapUtils.js"; import { LANDUSE } from "./landuseCodes.js"; export function buildFeatureLanduse(ctx) { const { seed, elevation, slope, sea, river, floodplain, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, developable, ruralSuitability, valleySettlement, coastalSettlement, modernCities, logisticsParks, roadLanduseInfluence, roadInfluence, roadDensityInfluence, railInfluence2, stationInfluence, stationDensityInfluence, villageInfluence, cityInfluence, coreInfluence, oldTownInfluence, townInfluence, industrialInfluence, logisticsInfluence, } = ctx; const populationDensity = new Float32Array(SIZE); const landuse = new Uint8Array(SIZE); // Re-run land-use classification after landuse allocation. The loop above is // intentionally inside a helper to keep all thresholds in one place. function classifyLanduse() { landuse.fill(LANDUSE.RURAL); let maxDensity = 0; const baseNoiseSeed = seed + 15000; const urbanCapacity = new Float32Array(SIZE); const ruralDensityFloor = 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 transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82); const densityTransport = Math.max(roadDensityInfluence[i] * 0.95, stationDensityInfluence[i] * 1.05, railInfluence2[i] * 0.85); const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.30 + roadInfluence[i] * 0.14 + railInfluence2[i] * 0.10; const core = coreInfluence[i]; 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 + roadInfluence[i] * 0.14 + roadDensityInfluence[i] * 0.16 + transport * 0.08 + riverUrban * 0.14 - slope[i] * 0.18 - ridgeField[i] * 0.12 - floodplain[i] * 0.08 ); const highPenaltyDensity = Math.max(0, elevation[i] - 0.58); const agrarianDensity = clamp( agriculture[i] * 0.045 + ruralSuitability[i] * 0.035 + developable[i] * 0.028 + plain[i] * 0.018 + basinField[i] * 0.014 + valleySettlement[i] * 0.014 + coastalSettlement[i] * 0.012 + villageInfluence[i] * 0.040 + townInfluence[i] * 0.022 + roadDensityInfluence[i] * 0.038 + stationDensityInfluence[i] * 0.020 + railInfluence2[i] * 0.012 - slope[i] * 0.030 - ridgeField[i] * 0.020 - highPenaltyDensity * 0.058 ); const remoteWilderness = elevation[i] > 0.60 && slope[i] > 0.34 && ridgeField[i] > 0.38 && densityTransport < 0.035 && villageInfluence[i] < 0.025 && townInfluence[i] < 0.025 && cityInfluence[i] < 0.025; ruralDensityFloor[i] = remoteWilderness ? 0 : clamp(agrarianDensity, 0, elevation[i] > 0.62 || slope[i] > 0.42 ? 0.052 : 0.105); populationDensity[i] = clamp( urban * 0.66 + core * 0.46 + oldTown * 0.28 + townInfluence[i] * 0.16 + villageInfluence[i] * 0.14 + roadDensityInfluence[i] * 0.42 + stationDensityInfluence[i] * 0.34 + railInfluence2[i] * 0.12 + transport * 0.05 + agrarianDensity * 0.34 ); 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; continue; } if (industrialInfluence[i] > 0.22 && urbanCapacity[i] > 0.10) { landuse[i] = LANDUSE.INDUSTRIAL; continue; } if (logisticsInfluence[i] > 0.24 && urbanCapacity[i] > 0.08 && (roadInfluence[i] > 0.08 || railInfluence2[i] > 0.06)) { landuse[i] = LANDUSE.LOGISTICS; 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 + roadLanduseInfluence[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 (roadLanduseInfluence[i] > 0.30 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.05 || cityInfluence[i] > 0.11)) { landuse[i] = LANDUSE.SUBURB; } else if (agriculture[i] > 0.16 || rural > 0.18 || (developable[i] > 0.13 && plain[i] > 0.13) || (basinField[i] > 0.18 && slope[i] < 0.34) || (coastalLowland[i] > 0.16 && slope[i] < 0.32)) { landuse[i] = LANDUSE.FARMLAND; } else { const usablePlain = slope[i] < 0.30 && (plain[i] > 0.18 || developable[i] > 0.20 || basinField[i] > 0.20 || coastalLowland[i] > 0.18); landuse[i] = elevation[i] > 0.58 || slope[i] > 0.38 ? LANDUSE.FOREST : usablePlain ? LANDUSE.FARMLAND : 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(roadLanduseInfluence[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[i + dy * MAP_W + dx]; 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] = LANDUSE.SUBURB; } } 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; } } } for (const park of logisticsParks) { const r = 3; for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = park.x + dx; const y = park.y + dy; if (!inside(x, y)) continue; const i = indexOf(x, y); if (sea[i] || landuse[i] === LANDUSE.CBD || landuse[i] === LANDUSE.FOREST) continue; if (Math.hypot(dx, dy) <= r && (agriculture[i] > 0.18 || plain[i] > 0.15 || roadInfluence[i] > 0.06 || railInfluence2[i] > 0.05)) { landuse[i] = LANDUSE.LOGISTICS; } } } } if (maxDensity > 0) { for (let i = 0; i < SIZE; i++) { if (sea[i]) continue; const lu = landuse[i]; let floor = ruralDensityFloor[i]; if (lu === LANDUSE.FARMLAND) { floor = Math.max(floor, clamp(0.024 + agriculture[i] * 0.044 + ruralSuitability[i] * 0.024 + roadDensityInfluence[i] * 0.030 + stationDensityInfluence[i] * 0.026 + villageInfluence[i] * 0.018, 0, 0.110)); } else if (lu === LANDUSE.LOGISTICS) { floor = Math.max(floor, clamp(0.018 + roadDensityInfluence[i] * 0.026 + railInfluence2[i] * 0.014 + logisticsInfluence[i] * 0.012, 0, 0.060)); } else if (lu === LANDUSE.RURAL) { floor = Math.max(floor, clamp(0.012 + ruralSuitability[i] * 0.020 + developable[i] * 0.014 + roadDensityInfluence[i] * 0.024 + stationDensityInfluence[i] * 0.020, 0, 0.070)); } else if (lu === LANDUSE.FOREST) { floor = Math.min(floor, (roadDensityInfluence[i] > 0.04 || villageInfluence[i] > 0.03) ? 0.026 : 0); } const normalized = populationDensity[i] / maxDensity; populationDensity[i] = clamp(Math.max(normalized, floor)); if (lu === LANDUSE.FOREST && floor === 0 && populationDensity[i] < 0.012) populationDensity[i] = 0; } } } classifyLanduse(); for (const city of modernCities) { let urbanFootprintCells = 0; let coreFootprintCells = 0; const r = Math.ceil((city.urbanRadius || 8) * 1.3); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = city.x + dx; const y = city.y + dy; if (!inside(x, y)) continue; const i = indexOf(x, y); if (sea[i]) continue; if (Math.hypot(dx, dy) > r) continue; if (landuse[i] >= LANDUSE.OLD_URBAN && landuse[i] <= LANDUSE.ROADSIDE) urbanFootprintCells++; if (landuse[i] === LANDUSE.CBD) coreFootprintCells++; } } city.urbanFootprintCells = urbanFootprintCells; city.coreFootprintCells = coreFootprintCells; } return { landuse, populationDensity }; }