city tweaks
This commit is contained in:
parent
15c1031d72
commit
d762a88b22
12 changed files with 255 additions and 195 deletions
133
mapFeaturesV2.js
133
mapFeaturesV2.js
|
|
@ -204,11 +204,10 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
|
||||
function visibilityFactor(regionId, st) {
|
||||
if (regionId === 0) return 1.15;
|
||||
if (!st || st.area <= 0) return 0;
|
||||
// Small map-edge slivers should not get the same municipal/human density
|
||||
// as full neighboring prefectures. This keeps external regions legible.
|
||||
return clamp(Math.sqrt(st.area / 1700), 0.28, 0.92);
|
||||
// Treat the focused prefecture and neighboring prefectures with the same
|
||||
// density curve. Only genuinely clipped map-edge slivers are downscaled.
|
||||
return clamp(Math.sqrt(st.area / 1900), 0.32, 1.05);
|
||||
}
|
||||
|
||||
function pickRegionalPoints(scoreArray, {
|
||||
|
|
@ -316,8 +315,8 @@ export function generateMapFeatures(seed, terrain) {
|
|||
if (!st || st.developableCells < 10) return 0;
|
||||
const vf = visibilityFactor(regionId, st);
|
||||
const raw = (st.developableCells / 65 + st.valleyCells / 44 + st.coastCells / 55 + 1.2) * vf;
|
||||
const min = regionId === 0 ? 10 : st.area > 1100 ? 3 : st.area > 280 ? 1 : 0;
|
||||
const max = regionId === 0 ? 30 : st.area > 1800 ? 13 : st.area > 600 ? 7 : 3;
|
||||
const min = st.area > 2600 ? 7 : st.area > 1400 ? 4 : st.area > 520 ? 2 : st.area > 220 ? 1 : 0;
|
||||
const max = st.area > 3600 ? 24 : st.area > 2200 ? 17 : st.area > 900 ? 9 : 4;
|
||||
return Math.round(clamp(raw + rand(seed, 1033 + regionId * 19) * 1.5, min, max));
|
||||
},
|
||||
}).map((p, n) => {
|
||||
|
|
@ -365,8 +364,8 @@ export function generateMapFeatures(seed, terrain) {
|
|||
if (!st || st.townCells < 8) return 0;
|
||||
const vf = visibilityFactor(regionId, st);
|
||||
const raw = (st.developableCells / 260 + st.valleyCells / 150 + st.coastCells / 160 + 0.8) * vf;
|
||||
const min = regionId === 0 ? 4 : st.area > 1300 ? 1 : 0;
|
||||
const max = regionId === 0 ? 11 : st.area > 1800 ? 5 : st.area > 650 ? 3 : 1;
|
||||
const min = st.area > 2600 ? 3 : st.area > 1200 ? 2 : st.area > 520 ? 1 : 0;
|
||||
const max = st.area > 3600 ? 9 : st.area > 2200 ? 7 : st.area > 800 ? 4 : 2;
|
||||
return Math.round(clamp(raw + rand(seed, 1043 + regionId * 23) * 0.8, min, max));
|
||||
},
|
||||
extraScore: (x, y, i) => (distanceToNearest(commercialPorts, x, y) < 8 ? 0.07 : 0) + confluenceField[i] * 0.08,
|
||||
|
|
@ -444,7 +443,9 @@ export function generateMapFeatures(seed, terrain) {
|
|||
const i = indexOf(p.x, p.y);
|
||||
const regionId = regionIdAt(p.x, p.y);
|
||||
if (regionId < 0) continue;
|
||||
const capacity = estimateUrbanCapacity(p, regionId === 0 ? 30 : 24, regionId === 0 ? 1.12 : 1.0);
|
||||
const st = regionStats.get(regionId);
|
||||
const cityRadius = st && st.area > 2400 ? 28 : st && st.area > 900 ? 24 : 20;
|
||||
const capacity = estimateUrbanCapacity(p, cityRadius, 1.0);
|
||||
const score =
|
||||
Math.log10(capacity + 1) * 0.72 +
|
||||
townSuitability[i] * 1.40 +
|
||||
|
|
@ -463,12 +464,14 @@ export function generateMapFeatures(seed, terrain) {
|
|||
const st = regionStats.get(regionId);
|
||||
if (!st || st.developableCells < 30) continue;
|
||||
const vf = visibilityFactor(regionId, st);
|
||||
const maxCities = regionId === 0
|
||||
? clamp(Math.round(3 + st.developableCells / 520 + rand(seed, 12100) * 2), 5, 9)
|
||||
: clamp(Math.round((st.developableCells / 850 + 0.8) * vf), st.area > 1500 ? 1 : 0, st.area > 2600 ? 4 : st.area > 950 ? 2 : 1);
|
||||
const maxCities = clamp(
|
||||
Math.round((st.developableCells / 720 + 0.9) * vf + rand(seed, 12100 + regionId * 17) * 1.2),
|
||||
st.area > 1600 ? 1 : 0,
|
||||
st.area > 3600 ? 6 : st.area > 2200 ? 4 : st.area > 900 ? 3 : 1
|
||||
);
|
||||
const selected = pickEntities(list, {
|
||||
max: maxCities,
|
||||
minDistance: regionId === 0 ? 16 : 18,
|
||||
minDistance: 17,
|
||||
threshold: 0,
|
||||
seed: seed + 12110 + regionId * 313,
|
||||
jitter: 0.02,
|
||||
|
|
@ -480,61 +483,34 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!modernCities.some((p) => inFocusedPrefecture(p))) {
|
||||
const focusCandidates = [...markets, ...commercialPorts, ...villages].filter((p) => inFocusedPrefecture(p));
|
||||
let fallback = focusCandidates.sort((a, b) => {
|
||||
const ai = indexOf(a.x, a.y);
|
||||
const bi = indexOf(b.x, b.y);
|
||||
return (townSuitability[bi] + developable[bi]) - (townSuitability[ai] + developable[ai]);
|
||||
})[0];
|
||||
if (!fallback) {
|
||||
let best = null;
|
||||
let bestScore = -INF;
|
||||
for (let y = 2; y < MAP_H - 2; y += 2) {
|
||||
for (let x = 2; x < MAP_W - 2; x += 2) {
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const score = townSuitability[i] + developable[i] + hash2(x, y, seed + 12199) * 0.04;
|
||||
if (score > bestScore) { bestScore = score; best = { x, y, score, kind: "Local City", regionId: 0 }; }
|
||||
}
|
||||
}
|
||||
fallback = best;
|
||||
}
|
||||
if (fallback) modernCities.push({
|
||||
...fallback,
|
||||
candidateKind: fallback.candidateKind || "fallback",
|
||||
score: fallback.score || 0.5,
|
||||
capacity: estimateUrbanCapacity(fallback, 30, 1.15),
|
||||
regionId: 0,
|
||||
});
|
||||
}
|
||||
// No focused-prefecture fallback: all prefecture regions use the same city
|
||||
// selection rules, so the highlighted region is not overwritten after the
|
||||
// regional pass.
|
||||
|
||||
modernCities.sort((a, b) => b.capacity - a.capacity || b.score - a.score);
|
||||
for (const [rank, city] of modernCities.entries()) {
|
||||
const isFocused = inFocusedPrefecture(city);
|
||||
const isPrefecturalCapital = isFocused && !modernCities.slice(0, rank).some((c) => c.isPrefecturalCapital);
|
||||
const isRegionalCapital = !isFocused && !modernCities.slice(0, rank).some((c) => c.regionId === city.regionId && c.isRegionalCapital);
|
||||
const rawPop = isPrefecturalCapital
|
||||
? 450000 + rand(seed, 12200) * 1150000
|
||||
: isRegionalCapital
|
||||
? 160000 + rand(seed, 12201 + city.regionId * 17) * 460000
|
||||
: 32000 + Math.pow(rand(seed, 12202 + rank * 19 + city.x), 0.7) * 260000;
|
||||
const capMultiplier = isPrefecturalCapital ? 1.22 : isRegionalCapital ? 1.08 : 1.0;
|
||||
const isFirstInRegion = !modernCities.slice(0, rank).some((c) => c.regionId === city.regionId);
|
||||
const isPrefecturalCapital = inFocusedPrefecture(city) && !modernCities.slice(0, rank).some((c) => c.isPrefecturalCapital);
|
||||
const isRegionalCapital = isFirstInRegion;
|
||||
const rawPop = isRegionalCapital
|
||||
? 150000 + rand(seed, 12201 + city.regionId * 17) * 520000
|
||||
: 32000 + Math.pow(rand(seed, 12202 + rank * 19 + city.x), 0.7) * 260000;
|
||||
const capMultiplier = isRegionalCapital ? 1.10 : 1.0;
|
||||
const population = Math.round(Math.min(rawPop, city.capacity * capMultiplier) / 1000) * 1000;
|
||||
city.population = Math.max(isPrefecturalCapital ? 260000 : isRegionalCapital ? 90000 : 24000, population);
|
||||
city.population = Math.max(isRegionalCapital ? 90000 : 24000, population);
|
||||
city.isPrefecturalCapital = isPrefecturalCapital;
|
||||
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(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.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.urbanRadius = clamp(5.8 + Math.sqrt(city.population) / 72, 8, isRegionalCapital ? 34 : 24);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 380, 2.2, isRegionalCapital ? 6.5 : 5.6);
|
||||
city.sprawlRadius = clamp(city.urbanRadius * (isRegionalCapital ? 1.45 : city.population >= 120000 ? 1.28 : 1.15), city.urbanRadius + 2, isRegionalCapital ? 44 : 30);
|
||||
city.urbanWeight = clamp(0.95 + Math.log10(Math.max(10000, city.population)) * 0.29, 1.08, 2.5);
|
||||
}
|
||||
|
||||
function cityPopulationCap(city) {
|
||||
const radius = city?.isPrefecturalCapital ? 34 : city?.isRegionalCapital ? 29 : city?.population >= 350000 ? 26 : 18;
|
||||
const bias = city?.isPrefecturalCapital ? 1.25 : city?.isRegionalCapital ? 1.12 : 1.0;
|
||||
const radius = city?.isRegionalCapital ? 29 : city?.population >= 350000 ? 26 : 18;
|
||||
const bias = city?.isRegionalCapital ? 1.12 : 1.0;
|
||||
return estimateUrbanCapacity(city, radius, bias);
|
||||
}
|
||||
|
||||
|
|
@ -583,7 +559,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
...markets.filter(inRegion).map((p) => ({ ...p, nodeWeight: 3.2 + (p.population || 0) / 25000 })),
|
||||
...commercialPorts.filter(inRegion).map((p) => ({ ...p, nodeWeight: p.portClass === "major" ? 6.5 : 4.6 })),
|
||||
...passes.filter(inRegion).map((p) => ({ ...p, nodeWeight: 2.2 })),
|
||||
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, regionId === 0 ? 18 : 10);
|
||||
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 16 : 10);
|
||||
}
|
||||
|
||||
const premodernRoads = [];
|
||||
|
|
@ -615,7 +591,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
if (nodes.length < 2) continue;
|
||||
const connected = [nodes[0]];
|
||||
const remaining = nodes.slice(1);
|
||||
const maxEdges = regionId === 0 ? Math.min(14, nodes.length + 3) : Math.min(7, nodes.length + 1);
|
||||
const maxEdges = (regionStats.get(regionId)?.area || 0) > 2200 ? Math.min(13, nodes.length + 3) : Math.min(7, nodes.length + 1);
|
||||
while (remaining.length && nationalRoads.length < 48) {
|
||||
let best = null;
|
||||
let bestScore = INF;
|
||||
|
|
@ -635,7 +611,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
|
||||
// A few k-nearest shortcuts for urbanized regions.
|
||||
const urbanNodes = nodes.filter((p) => p.population || p.portClass).slice(0, regionId === 0 ? 8 : 4);
|
||||
const urbanNodes = nodes.filter((p) => p.population || p.portClass).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 7 : 4);
|
||||
for (let i = 0; i < urbanNodes.length; i++) {
|
||||
const a = urbanNodes[i];
|
||||
const b = urbanNodes.slice(i + 1).sort((p, q) => Math.hypot(a.x - p.x, a.y - p.y) - Math.hypot(a.x - q.x, a.y - q.y))[0];
|
||||
|
|
@ -645,7 +621,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
|
||||
// Railways: only high-order cities/ports, as a lightweight placeholder.
|
||||
const railNodes = nodes.filter((p) => (p.population || 0) > 80000 || p.portClass === "major" || p.portClass === "regional").slice(0, regionId === 0 ? 7 : 4);
|
||||
const railNodes = nodes.filter((p) => (p.population || 0) > 80000 || p.portClass === "major" || p.portClass === "regional").slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 6 : 4);
|
||||
railNodes.sort((a, b) => a.x - b.x || a.y - b.y);
|
||||
for (let i = 1; i < railNodes.length; i++) {
|
||||
const path = routeLight(railNodes[i - 1], railNodes[i], 4);
|
||||
|
|
@ -672,7 +648,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
if (!sea[i] && regionIdAt(x, y) === regionId && (x < 5 || y < 5 || x > MAP_W - 6 || y > MAP_H - 6)) edgeCandidates.push({ x, y, score: developable[i] + valleySettlement[i] });
|
||||
}
|
||||
}
|
||||
const gateway = pickEntities(edgeCandidates, { max: regionId === 0 ? 2 : 1, minDistance: 16, seed: seed + 13200 + regionId * 11 })[0];
|
||||
const gateway = pickEntities(edgeCandidates, { max: (regionStats.get(regionId)?.area || 0) > 2200 ? 2 : 1, minDistance: 16, seed: seed + 13200 + regionId * 11 })[0];
|
||||
if (gateway) {
|
||||
gateway.kind = "External Gateway";
|
||||
gateway.regionId = regionId;
|
||||
|
|
@ -696,7 +672,12 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
}
|
||||
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads, ...expressways], 5);
|
||||
// Land-use road influence intentionally excludes expressways. Expressways
|
||||
// are through-corridors here, not automatic suburbanization generators.
|
||||
// A narrow field controls land-use attachment, while a broader field raises
|
||||
// population density around trunk roads without painting a wide suburb band.
|
||||
const roadLanduseInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads], 2.25);
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...externalRoads], 5.0);
|
||||
const railInfluence2 = influenceFromPaths([...railways, ...branchRailways, ...externalRailways], 4);
|
||||
|
||||
const stations = [];
|
||||
|
|
@ -739,7 +720,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
|
||||
for (const city of modernCities) {
|
||||
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.sprawlRadius || Math.round((city.urbanRadius || 10) * 1.4), (city.urbanWeight || 1.0) * (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");
|
||||
}
|
||||
|
|
@ -786,8 +767,8 @@ export function generateMapFeatures(seed, terrain) {
|
|||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
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 transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
|
||||
const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.26 + 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;
|
||||
|
|
@ -798,13 +779,13 @@ export function generateMapFeatures(seed, terrain) {
|
|||
basinField[i] * 0.16 +
|
||||
valleyField[i] * 0.16 +
|
||||
coastalLowland[i] * 0.12 +
|
||||
transport * 0.18 +
|
||||
roadInfluence[i] * 0.16 + transport * 0.10 +
|
||||
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);
|
||||
populationDensity[i] = clamp(urban * 0.66 + core * 0.46 + oldTown * 0.28 + townInfluence[i] * 0.16 + villageInfluence[i] * 0.14 + roadInfluence[i] * 0.18 + transport * 0.08);
|
||||
maxDensity = Math.max(maxDensity, populationDensity[i]);
|
||||
|
||||
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
|
||||
|
|
@ -825,13 +806,13 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
|
||||
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 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 (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 (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.24 || rural > 0.22 || (developable[i] > 0.18 && plain[i] > 0.18)) {
|
||||
landuse[i] = LANDUSE.FARMLAND;
|
||||
} else {
|
||||
|
|
@ -846,7 +827,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
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);
|
||||
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++) {
|
||||
|
|
@ -865,18 +846,12 @@ export function generateMapFeatures(seed, terrain) {
|
|||
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;
|
||||
landuse[i] = LANDUSE.SUBURB;
|
||||
}
|
||||
}
|
||||
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadInfluence[i] > 0.22) && urbanNeighbors >= 3 && landuse[i] <= LANDUSE.FARMLAND && urbanCapacity[i] > 0.09) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue