terrain tweak?
This commit is contained in:
parent
1be3d86da0
commit
8e1ec7b9ac
7 changed files with 3423 additions and 1328 deletions
479
mapOutput.js
479
mapOutput.js
|
|
@ -1,6 +1,6 @@
|
|||
import { createNameDebug } from "./names.js";
|
||||
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
|
||||
import { aStar, applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
|
||||
import { applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
|
||||
|
||||
export function finishMapOutput({
|
||||
seed,
|
||||
|
|
@ -24,6 +24,8 @@ export function finishMapOutput({
|
|||
settlementCluster,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
visibleRavineField,
|
||||
surfaceTextureField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
|
|
@ -115,396 +117,14 @@ export function finishMapOutput({
|
|||
return out;
|
||||
}
|
||||
|
||||
|
||||
function generatedNameStem(fullName) {
|
||||
return String(fullName || "").replace(/[都道府県市町村区]$/u, "");
|
||||
}
|
||||
|
||||
function terrainSettlementScore(x, y, sideBias = null) {
|
||||
const i = indexOf(x, y);
|
||||
if (!inside(x, y) || sea[i]) return -INF;
|
||||
const edgeBias = sideBias === "north" ? (MAP_H - y) / MAP_H
|
||||
: sideBias === "south" ? y / MAP_H
|
||||
: sideBias === "west" ? (MAP_W - x) / MAP_W
|
||||
: sideBias === "east" ? x / MAP_W
|
||||
: 0;
|
||||
return settlementCluster[i] * 0.55 + plain[i] * 0.42 + agriculture[i] * 0.22 + basinField[i] * 0.20 + coastalLowland[i] * 0.20 + valleyField[i] * 0.16 + edgeBias * 0.08 - slope[i] * 0.54 - ridgeField[i] * 0.22 - Math.max(0, elevation[i] - 0.62) * 1.25;
|
||||
}
|
||||
|
||||
function chooseSpacedPoints(candidates, count, minDistance, seedOffset = 0) {
|
||||
const selected = [];
|
||||
const ordered = candidates
|
||||
.map((p, n) => ({ ...p, score: (p.score || 0) + rand(seed, seedOffset + n * 17 + p.x * 5 + p.y * 7) * 0.06 }))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
for (const p of ordered) {
|
||||
if (selected.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < minDistance)) continue;
|
||||
selected.push(p);
|
||||
if (selected.length >= count) break;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
function labelCollisionScore(x, y, avoidPoints) {
|
||||
let nearest = 99;
|
||||
for (const p of avoidPoints) {
|
||||
if (!p) continue;
|
||||
const d = Math.hypot(x - p.x, y - p.y);
|
||||
nearest = Math.min(nearest, d);
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
const MAX_BRIDGE_CELLS = 7;
|
||||
|
||||
function pointPair(p) {
|
||||
if (Array.isArray(p)) return [p[0], p[1]];
|
||||
return [p.x, p.y];
|
||||
}
|
||||
|
||||
function sampledWaterRunBetween(a, b) {
|
||||
if (!a || !b) return 0;
|
||||
const [ax, ay] = pointPair(a);
|
||||
const [bx, by] = pointPair(b);
|
||||
const steps = Math.max(1, Math.ceil(Math.hypot(bx - ax, by - ay) * 1.6));
|
||||
let run = 0;
|
||||
let maxRun = 0;
|
||||
for (let k = 0; k <= steps; k++) {
|
||||
const t = k / steps;
|
||||
const x = clamp(Math.round(ax + (bx - ax) * t), 0, MAP_W - 1);
|
||||
const y = clamp(Math.round(ay + (by - ay) * t), 0, MAP_H - 1);
|
||||
const isWater = sea[indexOf(x, y)];
|
||||
if (isWater) {
|
||||
run++;
|
||||
maxRun = Math.max(maxRun, run);
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
return maxRun;
|
||||
}
|
||||
|
||||
function pathMaxWaterRun(path) {
|
||||
if (!path || path.length < 2) return 0;
|
||||
let maxRun = 0;
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
maxRun = Math.max(maxRun, sampledWaterRunBetween(path[k - 1], path[k]));
|
||||
}
|
||||
return maxRun;
|
||||
}
|
||||
|
||||
function landDetourCost(x, y) {
|
||||
if (!inside(x, y)) return INF;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
return Math.max(
|
||||
0.35,
|
||||
1 +
|
||||
slope[i] * 7.6 +
|
||||
Math.max(0, elevation[i] - 0.58) * 10.5 +
|
||||
ridgeField[i] * 2.2 -
|
||||
plain[i] * 0.45 -
|
||||
valleyField[i] * 0.54 -
|
||||
coastalLowland[i] * 0.38
|
||||
);
|
||||
}
|
||||
|
||||
function compactOutputPath(path) {
|
||||
const out = [];
|
||||
let last = "";
|
||||
for (const p of path || []) {
|
||||
const [x, y] = pointPair(p);
|
||||
const key = `${x},${y}`;
|
||||
if (key === last) continue;
|
||||
last = key;
|
||||
out.push([x, y]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function repairLongBridgeSegments(path) {
|
||||
const compact = compactOutputPath(path);
|
||||
if (compact.length < 2) return compact.length >= 3 ? compact : [];
|
||||
const out = [compact[0]];
|
||||
for (let k = 1; k < compact.length; k++) {
|
||||
const from = out[out.length - 1];
|
||||
const to = compact[k];
|
||||
if (sampledWaterRunBetween(from, to) <= MAX_BRIDGE_CELLS) {
|
||||
out.push(to);
|
||||
continue;
|
||||
}
|
||||
const detour = compactOutputPath(aStar({ x: from[0], y: from[1] }, { x: to[0], y: to[1] }, landDetourCost));
|
||||
if (detour.length >= 2 && pathMaxWaterRun(detour) <= MAX_BRIDGE_CELLS) {
|
||||
out.push(...detour.slice(1));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const repaired = compactOutputPath(out);
|
||||
return repaired.length >= 3 ? repaired : [];
|
||||
}
|
||||
|
||||
function enforceBridgeLimitList(paths) {
|
||||
return (paths || [])
|
||||
.map((path) => pathMaxWaterRun(path) > MAX_BRIDGE_CELLS ? repairLongBridgeSegments(path) : compactOutputPath(path))
|
||||
.filter((path) => path && path.length >= 3 && pathMaxWaterRun(path) <= MAX_BRIDGE_CELLS);
|
||||
}
|
||||
|
||||
function shoreContactScore(x, y) {
|
||||
let contacts = 0;
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
if (!dx && !dy) continue;
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
if (!sea[indexOf(nx, ny)]) contacts++;
|
||||
}
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
function labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset, preferSea = false) {
|
||||
const i = indexOf(x, y);
|
||||
const nearest = labelCollisionScore(x, y, avoidPoints);
|
||||
const capD = capital ? Math.hypot(x - capital.x, y - capital.y) : 24;
|
||||
const fallbackD = fallback ? Math.hypot(x - fallback.x, y - fallback.y) : 0;
|
||||
const terrainBias = sea[i]
|
||||
? 8 + Math.min(18, shoreContactScore(x, y)) * 0.45
|
||||
: plain[i] * 0.18 + basinField[i] * 0.10 + coastalLowland[i] * 0.10 - slope[i] * 0.08;
|
||||
const clearance = Math.min(nearest, 24) * 1.55 - Math.max(0, 8 - nearest) * 5.0;
|
||||
const capitalBias = capital ? -Math.max(0, capD - (preferSea ? 26 : 45)) * 0.09 : 0;
|
||||
return clearance + terrainBias + capitalBias - fallbackD * 0.012 + rand(seed, seedOffset + x * 17 + y * 19) * 0.42;
|
||||
}
|
||||
|
||||
function pickPrefectureLabelPosition(avoidPoints, fallback, options = {}) {
|
||||
const {
|
||||
capital = null,
|
||||
landPredicate = (x, y, i) => prefectureMask[i] && !sea[i],
|
||||
areaPredicate = null,
|
||||
seedOffset = 7461,
|
||||
} = options;
|
||||
let best = null;
|
||||
let bestScore = -INF;
|
||||
|
||||
// Prefer open water near the prefectural capital when it reads like a coastal prefecture label.
|
||||
if (capital) {
|
||||
const rMax = 30;
|
||||
for (let dy = -rMax; dy <= rMax; dy += 2) {
|
||||
for (let dx = -rMax; dx <= rMax; dx += 2) {
|
||||
const x = clamp(Math.round(capital.x + dx), 0, MAP_W - 1);
|
||||
const y = clamp(Math.round(capital.y + dy), 0, MAP_H - 1);
|
||||
if (x < 6 || y < 6 || x > MAP_W - 7 || y > MAP_H - 7) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (!sea[i]) continue;
|
||||
if (areaPredicate && !areaPredicate(x, y)) continue;
|
||||
const capD = Math.hypot(x - capital.x, y - capital.y);
|
||||
if (capD < 5 || capD > rMax) continue;
|
||||
const coastTouch = shoreContactScore(x, y);
|
||||
if (coastTouch < 3 || coastTouch > 20) continue;
|
||||
const score = labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset + 3000, true) + coastTouch * 0.35;
|
||||
if (score > bestScore) { bestScore = score; best = { x, y, placement: "sea" }; }
|
||||
}
|
||||
}
|
||||
if (best && bestScore >= 18) return best;
|
||||
}
|
||||
|
||||
best = null;
|
||||
bestScore = -INF;
|
||||
for (let y = 6; y < MAP_H - 6; y += 3) {
|
||||
for (let x = 6; x < MAP_W - 6; x += 3) {
|
||||
const i = indexOf(x, y);
|
||||
if (!landPredicate(x, y, i)) continue;
|
||||
const score = labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset, false);
|
||||
if (score > bestScore) { bestScore = score; best = { x, y, placement: "land" }; }
|
||||
}
|
||||
}
|
||||
if (!best || bestScore < 10) return fallback;
|
||||
return best;
|
||||
}
|
||||
|
||||
function looseOutsidePath(a, b, salt, keepOutside = true) {
|
||||
if (!a || !b) return [];
|
||||
const steps = Math.max(4, Math.ceil(Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y))));
|
||||
const path = [];
|
||||
let lastKey = "";
|
||||
const bend = (rand(seed, 7600 + salt) - 0.5) * 5.5;
|
||||
const sideways = Math.abs(a.x - b.x) > Math.abs(a.y - b.y) ? "y" : "x";
|
||||
for (let k = 0; k <= steps; k++) {
|
||||
const t = k / steps;
|
||||
let x = Math.round(a.x + (b.x - a.x) * t);
|
||||
let y = Math.round(a.y + (b.y - a.y) * t);
|
||||
const wave = Math.sin(t * Math.PI) * bend;
|
||||
if (sideways === "y") y = Math.round(y + wave);
|
||||
else x = Math.round(x + wave);
|
||||
x = clamp(x, 0, MAP_W - 1);
|
||||
y = clamp(y, 0, MAP_H - 1);
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
if (keepOutside && prefectureMask[i]) continue;
|
||||
const key = `${x},${y}`;
|
||||
if (key === lastKey) continue;
|
||||
lastKey = key;
|
||||
path.push([x, y]);
|
||||
}
|
||||
return path.length >= 3 ? repairLongBridgeSegments(path) : [];
|
||||
}
|
||||
|
||||
function generatePrefectureIdentity(usedNamesForIdentity, avoidPoints) {
|
||||
const stemsA = ["青", "白", "黒", "高", "奥", "新", "東", "西", "南", "北", "中", "美", "豊", "若", "真", "清", "瑞", "長", "久", "安", "阿", "葛", "榛", "碓", "那", "鹿", "宇", "志", "遠", "羽"];
|
||||
const stemsB = ["森", "川", "野", "原", "沢", "島", "浦", "浜", "海", "山", "岳", "谷", "津", "崎", "里", "畑", "橋", "瀬", "井", "沼", "丘", "郷", "城", "坂", "泊", "戸", "湊", "庄"];
|
||||
const suffixes = ["県", "県", "県", "県", "県", "府"];
|
||||
const localUsed = new Set(usedNamesForIdentity || []);
|
||||
function pickGeneratedName(offset) {
|
||||
const a = stemsA[Math.floor(rand(seed, 7410 + offset * 11) * stemsA.length) % stemsA.length];
|
||||
const b = stemsB[Math.floor(rand(seed, 7420 + offset * 13) * stemsB.length) % stemsB.length];
|
||||
const c = suffixes[Math.floor(rand(seed, 7430 + offset * 17) * suffixes.length) % suffixes.length];
|
||||
return `${a}${b}${c}`;
|
||||
}
|
||||
function pickUniqueName(offset, preferred = null) {
|
||||
let candidate = preferred || pickGeneratedName(offset);
|
||||
let guard = 0;
|
||||
while (localUsed.has(candidate) && guard++ < 24) candidate = pickGeneratedName(offset + guard + 3);
|
||||
localUsed.add(candidate);
|
||||
if (usedNamesForIdentity) usedNamesForIdentity.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const capital = modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]);
|
||||
const capStem = generatedNameStem(capital?.name);
|
||||
const prefectureSuffix = suffixes[Math.floor(rand(seed, 7433) * suffixes.length) % suffixes.length];
|
||||
const capitalBased = capStem && rand(seed, 7440) < 0.80 ? `${capStem}${prefectureSuffix}` : null;
|
||||
const name = pickUniqueName(0, capitalBased);
|
||||
|
||||
let sx = 0, sy = 0, n = 0;
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
sx += x; sy += y; n++;
|
||||
}
|
||||
}
|
||||
const fallback = { x: n ? sx / n : MAP_W / 2, y: n ? sy / n : MAP_H / 2 };
|
||||
const labelPoint = pickPrefectureLabelPosition(avoidPoints, fallback, {
|
||||
capital,
|
||||
landPredicate: (x, y, i) => prefectureMask[i] && !sea[i],
|
||||
seedOffset: 7461,
|
||||
});
|
||||
const label = { name, x: labelPoint.x, y: labelPoint.y, kind: "Prefecture Label", placement: labelPoint.placement || "land" };
|
||||
|
||||
const sideSamples = { north: [], south: [], west: [], east: [] };
|
||||
for (let x = 2; x < MAP_W - 2; x += 4) {
|
||||
for (let y = 0; y < Math.min(13, MAP_H); y++) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.north.push({ x, y: 3 }); break; }
|
||||
for (let y = MAP_H - 1; y >= Math.max(0, MAP_H - 13); y--) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.south.push({ x, y: MAP_H - 4 }); break; }
|
||||
}
|
||||
for (let y = 2; y < MAP_H - 2; y += 4) {
|
||||
for (let x = 0; x < Math.min(13, MAP_W); x++) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.west.push({ x: 3, y }); break; }
|
||||
for (let x = MAP_W - 1; x >= Math.max(0, MAP_W - 13); x--) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.east.push({ x: MAP_W - 4, y }); break; }
|
||||
}
|
||||
|
||||
const neighbors = [];
|
||||
const sideOrder = ["north", "east", "south", "west"];
|
||||
for (let si = 0; si < sideOrder.length; si++) {
|
||||
const side = sideOrder[si];
|
||||
const samples = sideSamples[side];
|
||||
if (!samples.length && rand(seed, 7500 + si) < 0.45) continue;
|
||||
const nm = pickUniqueName(si + 1);
|
||||
const p = samples.length ? samples[Math.floor(rand(seed, 7520 + si) * samples.length) % samples.length] : (
|
||||
side === "north" ? { x: MAP_W * 0.5, y: 3 } : side === "south" ? { x: MAP_W * 0.5, y: MAP_H - 4 } : side === "west" ? { x: 3, y: MAP_H * 0.5 } : { x: MAP_W - 4, y: MAP_H * 0.5 }
|
||||
);
|
||||
neighbors.push({ name: nm, side, x: p.x, y: p.y, kind: "Neighbor Prefecture Label" });
|
||||
}
|
||||
return { name, label, neighbors };
|
||||
}
|
||||
|
||||
function generateNeighborPrefectureDetails(neighborLabels, usedNamesForNeighbor, nameDebugForNeighbor) {
|
||||
const allCities = [];
|
||||
const allAdmins = [];
|
||||
const allCbds = [];
|
||||
const allRoads = [];
|
||||
const allRailways = [];
|
||||
const details = [];
|
||||
const sideBand = {
|
||||
north: (x, y) => y <= Math.floor(MAP_H * 0.30),
|
||||
south: (x, y) => y >= Math.ceil(MAP_H * 0.70),
|
||||
west: (x, y) => x <= Math.floor(MAP_W * 0.34),
|
||||
east: (x, y) => x >= Math.ceil(MAP_W * 0.66),
|
||||
};
|
||||
for (let ni = 0; ni < neighborLabels.length; ni++) {
|
||||
const label = neighborLabels[ni];
|
||||
const predicate = sideBand[label.side] || (() => true);
|
||||
const candidates = [];
|
||||
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] || !predicate(x, y)) continue;
|
||||
const dToLabel = Math.hypot(x - label.x, y - label.y);
|
||||
const borderAway = label.side === "north" ? y : label.side === "south" ? MAP_H - 1 - y : label.side === "west" ? x : MAP_W - 1 - x;
|
||||
const score = terrainSettlementScore(x, y, label.side) - dToLabel * 0.006 + Math.min(16, borderAway) * 0.006;
|
||||
if (score > 0.06) candidates.push({ x, y, score, neighborIndex: ni, neighborName: label.name, side: label.side });
|
||||
}
|
||||
}
|
||||
const rawCities = chooseSpacedPoints(candidates, 2 + Math.floor(rand(seed, 7700 + ni) * 3), 12, 7710 + ni * 100)
|
||||
.map((p, n) => {
|
||||
const rank = n === 0 ? "Neighbor Prefectural Capital" : n === 1 ? "Neighbor Regional Center" : "Neighbor City";
|
||||
const popBase = n === 0 ? 240000 : n === 1 ? 90000 : 36000;
|
||||
const popSpread = n === 0 ? 620000 : n === 1 ? 220000 : 90000;
|
||||
const population = Math.round((popBase + popSpread * Math.pow(clamp(p.score + rand(seed, 7730 + ni * 31 + n), 0, 1), 1.8)) / 1000) * 1000;
|
||||
return { ...p, kind: rank, rank, population, urbanRadius: clamp(7 + Math.sqrt(population) / 105, 7, 21), coreRadius: clamp(2.4 + Math.sqrt(population) / 420, 2.4, 6.2), urbanWeight: 1.0 + Math.log10(Math.max(10000, population)) * 0.23, insidePrefecture: false };
|
||||
});
|
||||
const namedCities = attachIdsAndNames(rawCities, `neighborCity${ni}`, seed + ni * 100, null, nameFields, usedNamesForNeighbor, nameDebugForNeighbor);
|
||||
const adminCandidates = chooseSpacedPoints(candidates.filter((p) => !namedCities.some((c) => Math.hypot(c.x - p.x, c.y - p.y) < 7)), 4 + Math.floor(rand(seed, 7760 + ni) * 5), 8, 7770 + ni * 100)
|
||||
.map((p) => ({ ...p, kind: "Neighbor Municipal Center", insidePrefecture: false }));
|
||||
const namedAdmins = attachIdsAndNames(adminCandidates, `neighborAdmin${ni}`, seed + ni * 131, "Neighbor Municipal Center", nameFields, usedNamesForNeighbor, nameDebugForNeighbor);
|
||||
const cbds = namedCities.map((city, ci) => ({ x: city.x, y: city.y, parentId: city.id, parentName: city.name, name: `${city.name}CBD`, kind: ci === 0 ? "Neighbor Central Business District" : "Neighbor Urban Center", neighborIndex: ni, neighborName: label.name, insidePrefecture: false }));
|
||||
const nodes = [...namedCities, ...namedAdmins].sort((a, b) => (b.population || 0) - (a.population || 0) || (b.score || 0) - (a.score || 0));
|
||||
const roads = [];
|
||||
const rails = [];
|
||||
for (let i = 1; i < nodes.length; i++) {
|
||||
const target = nodes[i];
|
||||
const anchor = nodes.slice(0, i).sort((a, b) => Math.hypot(a.x - target.x, a.y - target.y) - Math.hypot(b.x - target.x, b.y - target.y))[0];
|
||||
const road = looseOutsidePath(anchor, target, ni * 400 + i * 17, true);
|
||||
if (road.length >= 3) roads.push(road);
|
||||
if (i <= 2 && rand(seed, 7790 + ni * 19 + i) > 0.30) {
|
||||
const rail = looseOutsidePath(anchor, target, ni * 500 + i * 23, true);
|
||||
if (rail.length >= 4) rails.push(rail);
|
||||
}
|
||||
}
|
||||
if (nodes[0]) {
|
||||
const gate = { x: label.x, y: label.y };
|
||||
const gatewayRoad = looseOutsidePath(nodes[0], gate, ni * 600 + 7, true);
|
||||
if (gatewayRoad.length >= 3) roads.push(gatewayRoad);
|
||||
}
|
||||
const labelAvoid = [...namedCities, ...namedAdmins, ...allCities, ...allAdmins];
|
||||
const labelCandidates = candidates.length ? candidates : [{ x: label.x, y: label.y, score: 0.1 }];
|
||||
const fallbackLabel = chooseSpacedPoints(labelCandidates, 1, 1, 7810 + ni)[0] || label;
|
||||
const capitalPoint = namedCities[0] || fallbackLabel;
|
||||
const betterLabel = pickPrefectureLabelPosition(labelAvoid, fallbackLabel, {
|
||||
capital: capitalPoint,
|
||||
landPredicate: (x, y, i) => !prefectureMask[i] && !sea[i] && predicate(x, y),
|
||||
areaPredicate: predicate,
|
||||
seedOffset: 7810 + ni * 97,
|
||||
}) || fallbackLabel;
|
||||
label.x = betterLabel.x;
|
||||
label.y = betterLabel.y;
|
||||
label.placement = betterLabel.placement || "land";
|
||||
allCities.push(...namedCities);
|
||||
allAdmins.push(...namedAdmins);
|
||||
allCbds.push(...cbds);
|
||||
allRoads.push(...roads);
|
||||
allRailways.push(...rails);
|
||||
details.push({ ...label, cities: namedCities, adminCenters: namedAdmins, centralBusinessDistricts: cbds, roads, railways: rails });
|
||||
}
|
||||
return { prefectures: details, cities: allCities, adminCenters: allAdmins, centralBusinessDistricts: allCbds, roads: allRoads, railways: allRailways };
|
||||
}
|
||||
|
||||
// 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);
|
||||
let abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
|
||||
const abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
|
||||
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
|
||||
let preservedOldRoads = premodernRoads.filter((_, i) => i % 2 === 0);
|
||||
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();
|
||||
|
|
@ -585,96 +205,10 @@ export function finishMapOutput({
|
|||
...externalGateways,
|
||||
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
|
||||
|
||||
const avoidForPrefectureLabel = [
|
||||
...modernCities,
|
||||
...ports,
|
||||
...markets,
|
||||
...castles,
|
||||
...castleTowns,
|
||||
...adminCenters,
|
||||
...stations,
|
||||
...satelliteCities,
|
||||
...newTowns,
|
||||
].filter((p) => p.insidePrefecture && p.name);
|
||||
const prefectureIdentity = generatePrefectureIdentity(usedNames, avoidForPrefectureLabel);
|
||||
const neighborPrefectureDetails = generateNeighborPrefectureDetails(prefectureIdentity.neighbors, usedNames, nameDebug);
|
||||
|
||||
const bridgeLimitedPathGroups = [
|
||||
"premodernRoads",
|
||||
"minorRoads",
|
||||
"nationalRoads",
|
||||
"ringRoads",
|
||||
"expressways",
|
||||
"ringExpressways",
|
||||
"icAccessRoads",
|
||||
"externalRoads",
|
||||
"externalExpressways",
|
||||
"railways",
|
||||
"branchRailways",
|
||||
"ringRailways",
|
||||
"externalRailways",
|
||||
"abandonedRailways",
|
||||
"preservedOldRoads",
|
||||
];
|
||||
premodernRoads = enforceBridgeLimitList(premodernRoads);
|
||||
minorRoads = enforceBridgeLimitList(minorRoads);
|
||||
nationalRoads = enforceBridgeLimitList(nationalRoads);
|
||||
ringRoads = enforceBridgeLimitList(ringRoads);
|
||||
expressways = enforceBridgeLimitList(expressways);
|
||||
ringExpressways = enforceBridgeLimitList(ringExpressways);
|
||||
icAccessRoads = enforceBridgeLimitList(icAccessRoads);
|
||||
externalRoads = enforceBridgeLimitList(externalRoads);
|
||||
externalExpressways = enforceBridgeLimitList(externalExpressways);
|
||||
railways = enforceBridgeLimitList(railways);
|
||||
branchRailways = enforceBridgeLimitList(branchRailways);
|
||||
ringRailways = enforceBridgeLimitList(ringRailways);
|
||||
externalRailways = enforceBridgeLimitList(externalRailways);
|
||||
abandonedRailways = enforceBridgeLimitList(abandonedRailways);
|
||||
preservedOldRoads = enforceBridgeLimitList(preservedOldRoads);
|
||||
for (const pref of neighborPrefectureDetails.prefectures || []) {
|
||||
pref.roads = enforceBridgeLimitList(pref.roads);
|
||||
pref.railways = enforceBridgeLimitList(pref.railways);
|
||||
}
|
||||
neighborPrefectureDetails.roads = enforceBridgeLimitList(neighborPrefectureDetails.roads);
|
||||
neighborPrefectureDetails.railways = enforceBridgeLimitList(neighborPrefectureDetails.railways);
|
||||
const bridgeLimitDebug = {
|
||||
maxBridgeCells: MAX_BRIDGE_CELLS,
|
||||
maxWaterRun: Math.max(
|
||||
0,
|
||||
...[
|
||||
...premodernRoads,
|
||||
...minorRoads,
|
||||
...nationalRoads,
|
||||
...ringRoads,
|
||||
...expressways,
|
||||
...ringExpressways,
|
||||
...icAccessRoads,
|
||||
...externalRoads,
|
||||
...externalExpressways,
|
||||
...railways,
|
||||
...branchRailways,
|
||||
...ringRailways,
|
||||
...externalRailways,
|
||||
...abandonedRailways,
|
||||
...preservedOldRoads,
|
||||
...(neighborPrefectureDetails.roads || []),
|
||||
...(neighborPrefectureDetails.railways || []),
|
||||
].map(pathMaxWaterRun)
|
||||
),
|
||||
enforcedGroups: bridgeLimitedPathGroups,
|
||||
};
|
||||
|
||||
return applyOutputOptions({
|
||||
width: MAP_W,
|
||||
height: MAP_H,
|
||||
cellSize: CELL_SIZE,
|
||||
// Keep the apparent map scale close to the original 172-cell-wide version
|
||||
// after increasing logical terrain resolution.
|
||||
scaleKmPerCell: 172 / MAP_W,
|
||||
prefectureName: prefectureIdentity.name,
|
||||
prefectureLabel: prefectureIdentity.label,
|
||||
neighborPrefectures: prefectureIdentity.neighbors,
|
||||
neighborPrefectureDetails,
|
||||
terrainTemplate,
|
||||
seaLevel,
|
||||
prefectureMask,
|
||||
|
|
@ -696,6 +230,8 @@ export function finishMapOutput({
|
|||
settlementCluster,
|
||||
ridgeField,
|
||||
valleyField,
|
||||
visibleRavineField,
|
||||
surfaceTextureField,
|
||||
basinField,
|
||||
coastalLowland,
|
||||
flowAccum,
|
||||
|
|
@ -754,7 +290,6 @@ export function finishMapOutput({
|
|||
smallStreams,
|
||||
externalGateways,
|
||||
transportDebug,
|
||||
bridgeLimitDebug,
|
||||
entitiesForNames,
|
||||
nameDebug,
|
||||
}, options);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue