213 lines
10 KiB
JavaScript
213 lines
10 KiB
JavaScript
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 };
|
|
}
|