?
This commit is contained in:
parent
3ac2c116bd
commit
da9d8ef904
11 changed files with 612 additions and 344 deletions
|
|
@ -1057,13 +1057,20 @@ export function splitOversizedRuralMunicipalities(adminId, prefectureMask, sea,
|
|||
}
|
||||
|
||||
let splitMunicipalities = 0;
|
||||
let rejectedMunicipalities = 0;
|
||||
for (const [id, cells] of area) {
|
||||
const averageLowland = (lowland.get(id) || 0) / cells;
|
||||
const averageMountain = (mountain.get(id) || 0) / cells;
|
||||
if (cells < median * 2.25 || averageLowland < 0.28 || averageMountain > 0.44) continue;
|
||||
if (cells < median * 1.85 || averageLowland < 0.24 || averageMountain > 0.48) {
|
||||
if (cells >= median * 1.85) rejectedMunicipalities++;
|
||||
continue;
|
||||
}
|
||||
const localSettlements = settlements.filter((p) => p && inside(p.x, p.y) && adminId[indexOf(p.x, p.y)] === id);
|
||||
const meaningfulNodes = localSettlements.filter((p) => p.kind === "Satellite City" || p.kind === "New Town" || p.kind === "Market Town" || (p.population || 0) >= 30000);
|
||||
if (meaningfulNodes.length < 2) continue;
|
||||
if (meaningfulNodes.length < 2) {
|
||||
rejectedMunicipalities++;
|
||||
continue;
|
||||
}
|
||||
let changedHere = 0;
|
||||
for (const unit of compartments) {
|
||||
if (!unit || unit.area === 0 || unitOwner[unit.id] !== id) continue;
|
||||
|
|
@ -1094,7 +1101,7 @@ export function splitOversizedRuralMunicipalities(adminId, prefectureMask, sea,
|
|||
repairAdminTopology(adminId, prefectureMask, sea, adminCenters, naturalBarrierScore, populationDensity, landuse);
|
||||
let changedCells = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && before[i] !== adminId[i]) changedCells++;
|
||||
return { changedCells, splitMunicipalities };
|
||||
return { changedCells, splitMunicipalities, rejectedMunicipalities };
|
||||
}
|
||||
|
||||
export function splitOversizedLowlandMunicipalities(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCenters = [], settlements = []) {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ function municipalityAreaById(adminId, prefectureMask, sea) {
|
|||
return area;
|
||||
}
|
||||
|
||||
function computeTargetMunicipalityCount({ prefectureMask, sea, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages }) {
|
||||
function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages }) {
|
||||
let landCells = 0;
|
||||
let habitableCells = 0;
|
||||
let lowlandCells = 0;
|
||||
let coastlineComplexity = 0;
|
||||
let mountainCells = 0;
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
|
|
@ -36,7 +37,8 @@ function computeTargetMunicipalityCount({ prefectureMask, sea, slope, ridgeField
|
|||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
landCells++;
|
||||
if (slope[i] < 0.42 && ridgeField[i] < 0.55) habitableCells++;
|
||||
if (slope[i] < 0.42 && ridgeField[i] < 0.55 && (!elevation || elevation[i] < 0.72)) habitableCells++;
|
||||
if ((coastalLowland[i] > 0.20 || basinField[i] > 0.24) && slope[i] < 0.36 && ridgeField[i] < 0.52) lowlandCells++;
|
||||
if (ridgeField[i] > 0.52 || slope[i] > 0.48) mountainCells++;
|
||||
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
|
||||
const ni = indexOf(nx, ny);
|
||||
|
|
@ -48,10 +50,12 @@ function computeTargetMunicipalityCount({ prefectureMask, sea, slope, ridgeField
|
|||
}
|
||||
}
|
||||
const independentSatellites = (satelliteCities || []).filter((p) => p.municipalityClass === "independentSatelliteMunicipality").length;
|
||||
const settlementNodes = modernCities.length * 1.25 + markets.length * 0.9 + ports.length * 0.7 + independentSatellites * 0.8 + villages.length * 0.35;
|
||||
const settlementWeight = modernCities.length * 1.6 + markets.length * 1.0 + ports.length * 0.8 + independentSatellites * 0.7 + villages.length * 0.25;
|
||||
const basinBonus = Math.min(8, [...basinField].filter((v, i) => prefectureMask[i] && !sea[i] && v > 0.34).length / 520);
|
||||
const mountainRatio = landCells ? mountainCells / landCells : 0;
|
||||
return clamp(Math.round(habitableCells / 260 + settlementNodes * 0.45 + coastlineComplexity * 0.04 + basinBonus + mountainRatio * 4), 18, 48);
|
||||
const lowlandBonus = Math.min(7, lowlandCells / 430);
|
||||
const target = Math.round(habitableCells / 230 + settlementWeight + coastlineComplexity * 0.03 + basinBonus * 0.55 + lowlandBonus - mountainRatio * 2.2);
|
||||
return clamp(target, 20, 50);
|
||||
}
|
||||
|
||||
function estimateUrbanComponentArea(city, prefectureMask, sea, landuse, populationDensity) {
|
||||
|
|
@ -267,7 +271,7 @@ export function generateAdminLayout({
|
|||
return !nearMajor && !nearSmallUrban;
|
||||
});
|
||||
const satelliteClassificationDebug = classifySatelliteMunicipalities(satelliteCities, modernCities, prefectureMask, sea, landuse, populationDensity, roadInfluence, railInfluence2, boundaryRidgeField, river, flowAccum);
|
||||
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages });
|
||||
const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, modernCities, markets, ports, satelliteCities, villages });
|
||||
const satelliteMunicipalSeeds = (satelliteCities || [])
|
||||
.filter((city) => prefectureMask[indexOf(city.x, city.y)] && city.municipalityClass === "independentSatelliteMunicipality")
|
||||
.map((city) => ({ x: city.x, y: city.y, score: 1.05 + (city.population || 40000) / 260000, protectedSatellite: city }));
|
||||
|
|
@ -310,8 +314,12 @@ export function generateAdminLayout({
|
|||
changedAfterFinalMerge: 0,
|
||||
targetMunicipalityCount,
|
||||
actualMunicipalityCount: 0,
|
||||
municipalityCountReason: "habitable cells, settlement weight, coastline complexity, basin/lowland bonus, and mountain-ratio adjustment",
|
||||
changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0,
|
||||
oversizedRuralSplits: 0,
|
||||
oversizedLowlandSplits: 0,
|
||||
ruralSplitsAccepted: 0,
|
||||
ruralSplitsRejected: 0,
|
||||
satelliteMunicipalitiesCreated: satelliteMunicipalSeeds.length,
|
||||
satelliteMunicipalitiesMerged: 0,
|
||||
satelliteMunicipalitiesExpanded: 0,
|
||||
|
|
@ -404,6 +412,9 @@ export function generateAdminLayout({
|
|||
adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells;
|
||||
adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities;
|
||||
adminDebug.oversizedRuralSplits = oversizedSplitDebug.splitMunicipalities;
|
||||
adminDebug.oversizedLowlandSplits = oversizedSplitDebug.splitMunicipalities;
|
||||
adminDebug.ruralSplitsAccepted = oversizedSplitDebug.splitMunicipalities;
|
||||
adminDebug.ruralSplitsRejected = oversizedSplitDebug.rejectedMunicipalities || 0;
|
||||
previousSnapshot = new Int16Array(adminId);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 2);
|
||||
markChanged("changedAfterSnap");
|
||||
|
|
@ -448,6 +459,8 @@ export function generateAdminLayout({
|
|||
Object.assign(adminDebug, landscapeDebug);
|
||||
adminDebug.naturalCompartmentCount = adminDebug.naturalCompartmentCount || adminDebug.compartmentCount || 0;
|
||||
adminDebug.actualMunicipalityCount = new Set([...adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size;
|
||||
adminDebug.borderNaturalBarrierAverage = adminDebug.finalBorderNaturalBarrierAverage ?? 0;
|
||||
adminDebug.voronoiLikeRate = adminDebug.voronoiLikeRateAfter ?? 0;
|
||||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -971,6 +971,88 @@ export function generateMapFeatures(seed, terrain) {
|
|||
}
|
||||
});
|
||||
|
||||
const requiredTransportNodes = [];
|
||||
function addRequiredTransportNode(node, reason) {
|
||||
if (!node || !inside(node.x, node.y) || sea[indexOf(node.x, node.y)]) return;
|
||||
const key = `${node.x},${node.y}`;
|
||||
if (requiredTransportNodes.some((p) => `${p.x},${p.y}` === key)) return;
|
||||
requiredTransportNodes.push({ ...node, requiredTransportReason: reason });
|
||||
}
|
||||
addRequiredTransportNode(capital, "capital");
|
||||
for (const gate of externalGateways) addRequiredTransportNode(gate, "externalGateway");
|
||||
for (const city of modernCities) if ((city.population || 0) >= 120000 || city.isPrefecturalCapital) addRequiredTransportNode(city, "majorCity");
|
||||
for (const port of majorPorts) addRequiredTransportNode(port, "majorPort");
|
||||
|
||||
const backboneAccess = new Map();
|
||||
function nodeKey(p) {
|
||||
return `${p.x},${p.y}`;
|
||||
}
|
||||
function backbonePoint(node) {
|
||||
const key = nodeKey(node);
|
||||
if (!backboneAccess.has(key)) {
|
||||
const mode = node.requiredTransportReason === "externalGateway" ? "road" : "road";
|
||||
backboneAccess.set(key, routePoint(node, mode, 18000 + node.x * 97 + node.y * 101));
|
||||
}
|
||||
return backboneAccess.get(key);
|
||||
}
|
||||
function addBackboneRoad(a, b) {
|
||||
const start = backbonePoint(a);
|
||||
const goal = backbonePoint(b);
|
||||
const existing = [...nationalRoads, ...externalRoads, ...externalExpressways, ...railways, ...branchRailways];
|
||||
const path = aStar(start, goal, makeTransportCost(roadCost, existing, roadHubs, [start, goal], 2, 4.2, townAvoidNodes, 2.6, 4.2));
|
||||
if (path.length <= 3 || pathCompactness(path) > 3.8 || path.some(([x, y]) => elevation[indexOf(x, y)] > 0.72)) return false;
|
||||
nationalRoads.push(path);
|
||||
incrementDegree(roadDegree, a);
|
||||
incrementDegree(roadDegree, b);
|
||||
return true;
|
||||
}
|
||||
const connectedBackboneNodes = requiredTransportNodes.length ? [requiredTransportNodes[0]] : [];
|
||||
const pendingBackboneNodes = requiredTransportNodes.slice(1);
|
||||
let backboneEdgeCount = 0;
|
||||
while (pendingBackboneNodes.length && connectedBackboneNodes.length) {
|
||||
let bestIndex = -1;
|
||||
let bestAnchor = null;
|
||||
let bestScore = INF;
|
||||
for (let i = 0; i < pendingBackboneNodes.length; i++) {
|
||||
const node = pendingBackboneNodes[i];
|
||||
for (const anchor of connectedBackboneNodes) {
|
||||
const d = Math.hypot(node.x - anchor.x, node.y - anchor.y);
|
||||
const ai = indexOf(anchor.x, anchor.y);
|
||||
const bi = indexOf(node.x, node.y);
|
||||
const corridor = sameCorridorAffinity(anchor, node);
|
||||
const score = d * (1.0 - corridor * 0.22) + Math.max(elevation[ai], elevation[bi]) * 8 - Math.max(passSuitability[ai], passSuitability[bi]) * 4;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
bestAnchor = anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestIndex < 0 || !bestAnchor) break;
|
||||
const node = pendingBackboneNodes.splice(bestIndex, 1)[0];
|
||||
if (addBackboneRoad(bestAnchor, node)) backboneEdgeCount++;
|
||||
connectedBackboneNodes.push(node);
|
||||
}
|
||||
|
||||
function nearestPathCellDistance(node, paths) {
|
||||
let best = INF;
|
||||
for (const path of paths) {
|
||||
for (const [x, y] of path) best = Math.min(best, Math.hypot(node.x - x, node.y - y));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
const combinedModernBackbone = [...nationalRoads, ...externalRoads, ...externalExpressways, ...railways, ...branchRailways, ...externalRailways, ...expressways];
|
||||
const connectedRequiredNodeCount = requiredTransportNodes.filter((node) => nearestPathCellDistance(node, combinedModernBackbone) <= 7).length;
|
||||
const missingRequiredNodes = requiredTransportNodes
|
||||
.filter((node) => nearestPathCellDistance(node, combinedModernBackbone) > 7)
|
||||
.map((node) => ({ x: node.x, y: node.y, kind: node.kind, reason: node.requiredTransportReason }));
|
||||
const transportDebug = {
|
||||
requiredNodeCount: requiredTransportNodes.length,
|
||||
connectedRequiredNodeCount,
|
||||
missingRequiredNodes,
|
||||
backboneEdgeCount,
|
||||
};
|
||||
|
||||
function pruneHighMountainTransport(paths, threshold = 0.82) {
|
||||
for (let i = paths.length - 1; i >= 0; i--) {
|
||||
if (paths[i].some(([x, y]) => elevation[indexOf(x, y)] > threshold)) paths.splice(i, 1);
|
||||
|
|
@ -1327,6 +1409,7 @@ export function generateMapFeatures(seed, terrain) {
|
|||
railInfluence2,
|
||||
villageInfluence,
|
||||
externalGateways,
|
||||
transportDebug,
|
||||
cityPopulationCap,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -498,6 +498,7 @@ export function generateRegionalPrefectures(seed, sea, elevation, slope, river,
|
|||
compartmentCount: compartments.filter((unit) => unit.area > 0).length,
|
||||
changedAfterCompartmentAssignment: changed,
|
||||
borderNaturalBarrierAverage: afterNaturalAverage,
|
||||
voronoiLikeRate: afterVoronoiLikeRate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
57
mapOutput.js
57
mapOutput.js
|
|
@ -72,10 +72,12 @@ export function finishMapOutput({
|
|||
tributaryRivers,
|
||||
smallStreams,
|
||||
externalGateways,
|
||||
transportDebug,
|
||||
prefectureMask,
|
||||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
}) {
|
||||
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
|
||||
|
|
@ -171,28 +173,59 @@ export function finishMapOutput({
|
|||
center.name = best.name;
|
||||
}
|
||||
}
|
||||
const adminNamePrefixes = ["\u6771", "\u897F", "\u5357", "\u5317", "\u4E0A", "\u4E0B", "\u65B0", "\u65E7", "\u4E2D", "\u5916"];
|
||||
const adminNameSuffixes = [
|
||||
"\u753A\u57DF",
|
||||
"\u5E02\u57DF",
|
||||
"\u90F7\u57DF",
|
||||
"\u6D41\u57DF",
|
||||
"\u6E7E\u5CB8",
|
||||
"\u5C71\u9E93",
|
||||
"\u5E73\u91CE",
|
||||
"\u5730\u533A",
|
||||
];
|
||||
const adminNameCounts = new Map();
|
||||
for (const center of adminCenters) adminNameCounts.set(center.name, (adminNameCounts.get(center.name) || 0) + 1);
|
||||
const duplicateOrdinal = new Map();
|
||||
const baseVariantCounts = new Map();
|
||||
for (const center of adminCenters) {
|
||||
if ((adminNameCounts.get(center.name) || 0) <= 1) continue;
|
||||
const n = duplicateOrdinal.get(center.name) || 0;
|
||||
duplicateOrdinal.set(center.name, n + 1);
|
||||
const prefix = adminNamePrefixes[(Math.floor(center.x / Math.max(1, MAP_W / 3)) + Math.floor(center.y / Math.max(1, MAP_H / 3)) * 3 + n) % adminNamePrefixes.length];
|
||||
if (!String(center.name).startsWith(prefix)) center.name = `${prefix}${center.name}`;
|
||||
const base = String(center.name || "");
|
||||
const count = adminNameCounts.get(base) || 0;
|
||||
if (count <= 1) {
|
||||
baseVariantCounts.set(base, Math.max(baseVariantCounts.get(base) || 0, 1));
|
||||
continue;
|
||||
}
|
||||
const usedForBase = baseVariantCounts.get(base) || 0;
|
||||
if (usedForBase === 0) {
|
||||
baseVariantCounts.set(base, 1);
|
||||
continue;
|
||||
}
|
||||
if (usedForBase < 2) {
|
||||
const i = indexOf(center.x, center.y);
|
||||
const naturalSuffix = coastalLowland[i] > 0.28
|
||||
? "\u6E7E\u5CB8"
|
||||
: basinField[i] > 0.28
|
||||
? "\u5E73\u91CE"
|
||||
: ridgeField[i] > 0.42 || slope[i] > 0.34
|
||||
? "\u5C71\u9E93"
|
||||
: river[i] > 0.25 || flowAccum[i] > 0.38
|
||||
? "\u6D41\u57DF"
|
||||
: adminNameSuffixes[(Math.floor(center.x / 12) + Math.floor(center.y / 12)) % adminNameSuffixes.length];
|
||||
center.name = `${base}${naturalSuffix}`;
|
||||
center.derivedFromBaseName = base;
|
||||
nameDebug.derivedNameCount++;
|
||||
baseVariantCounts.set(base, usedForBase + 1);
|
||||
}
|
||||
}
|
||||
const usedAdminNames = new Set();
|
||||
for (const center of adminCenters) {
|
||||
let candidate = center.name;
|
||||
let guard = 0;
|
||||
while (usedAdminNames.has(candidate) && guard < adminNamePrefixes.length) {
|
||||
candidate = `${adminNamePrefixes[(guard + Math.floor(center.x / 12) + Math.floor(center.y / 12)) % adminNamePrefixes.length]}${center.name}`;
|
||||
guard++;
|
||||
const generated = String(center.generatedMunicipalityName || "");
|
||||
if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) {
|
||||
candidate = generated;
|
||||
}
|
||||
center.name = candidate;
|
||||
usedAdminNames.add(center.name);
|
||||
}
|
||||
nameDebug.maxDerivedPerBase = Math.max(0, ...baseVariantCounts.values());
|
||||
|
||||
const entitiesForNames = [
|
||||
...modernCities,
|
||||
|
|
@ -220,6 +253,7 @@ export function finishMapOutput({
|
|||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
elevation,
|
||||
moisture,
|
||||
|
|
@ -291,6 +325,7 @@ export function finishMapOutput({
|
|||
tributaryRivers,
|
||||
smallStreams,
|
||||
externalGateways,
|
||||
transportDebug,
|
||||
entitiesForNames,
|
||||
nameDebug,
|
||||
}, options);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
|
|
@ -53,7 +54,7 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
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,
|
||||
interchanges, logisticsParks, satelliteCities, newTowns, landuse, stationInfluence, roadInfluence, railInfluence2, villageInfluence, externalGateways, cityPopulationCap, transportDebug,
|
||||
} = features;
|
||||
|
||||
const { adminCentersRaw, adminId, adminBorders, adminDebug } = generateAdminLayout({
|
||||
|
|
@ -69,6 +70,6 @@ export function generateMap(seedInput = 114514, options = {}) {
|
|||
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,
|
||||
regionalDebug, terrainDebug, transportDebug,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1219,6 +1219,72 @@ export function generateTerrainAndRivers(seed) {
|
|||
}
|
||||
}
|
||||
|
||||
function countWaterComponents(mask, minArea = 1) {
|
||||
const seen = new Uint8Array(SIZE);
|
||||
let count = 0;
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (!mask[i] || seen[i]) continue;
|
||||
const queue = [i];
|
||||
seen[i] = 1;
|
||||
let area = 0;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
area++;
|
||||
const x = cur % MAP_W;
|
||||
const y = Math.floor(cur / MAP_W);
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!mask[ni] || seen[ni]) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
if (area >= minArea) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function countSmallLandIslands(maxArea = 8) {
|
||||
const seen = new Uint8Array(SIZE);
|
||||
let count = 0;
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (sea[i] || seen[i]) continue;
|
||||
const queue = [i];
|
||||
seen[i] = 1;
|
||||
let area = 0;
|
||||
let touchesEdge = false;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
area++;
|
||||
const x = cur % MAP_W;
|
||||
const y = Math.floor(cur / MAP_W);
|
||||
if (x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1) touchesEdge = true;
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni] || seen[ni]) continue;
|
||||
seen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
if (!touchesEdge && area <= maxArea) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const spineValues = [...arcSpineField].filter((_, i) => !sea[i]).sort((a, b) => b - a);
|
||||
const strongSpineSample = Math.max(1, Math.floor(spineValues.length * 0.05));
|
||||
const primarySpineStrength = spineValues.slice(0, strongSpineSample).reduce((sum, value) => sum + value, 0) / strongSpineSample;
|
||||
const riverConnectivityRate = mainRivers.length
|
||||
? mainRivers.filter((path) => path.some(([x, y], k) => k > path.length * 0.45 && neighbors8(x, y).some(([nx, ny]) => sea[indexOf(nx, ny)] || lake[indexOf(nx, ny)]))).length / mainRivers.length
|
||||
: 0;
|
||||
const depositionLowlandArea = [...depositionalLowland].filter((value, i) => !sea[i] && value > 0.24).length;
|
||||
const terrainDebug = {
|
||||
primarySpineStrength,
|
||||
riverConnectivityRate,
|
||||
smallIslandCount: countSmallLandIslands(8),
|
||||
largeInlandLakeCount: countWaterComponents(Float32Array.from(lake, (value) => value ? 1 : 0), 120),
|
||||
depositionLowlandArea,
|
||||
};
|
||||
|
||||
return {
|
||||
terrainTemplate,
|
||||
|
|
@ -1252,6 +1318,7 @@ export function generateTerrainAndRivers(seed) {
|
|||
prefectureBorder,
|
||||
prefectureRegionId,
|
||||
regionalDebug,
|
||||
terrainDebug,
|
||||
regionalPrefectureBorders,
|
||||
riverPaths,
|
||||
mainRivers,
|
||||
|
|
|
|||
10
names.js
10
names.js
|
|
@ -406,9 +406,13 @@ export function createNameDebug(probabilities = NAME_PROBABILITIES, pools = NAME
|
|||
generatedNamesUsed: 0,
|
||||
invalidNamesRejected: 0,
|
||||
oneCharacterNamesPrevented: 0,
|
||||
rejectedOneCharacterNames: 0,
|
||||
duplicateRetries: 0,
|
||||
fallbackAttempts: 0,
|
||||
legacyFallbackUsed: 0,
|
||||
oneKanjiAppendFallbackUsed: 0,
|
||||
derivedNameCount: 0,
|
||||
maxDerivedPerBase: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -514,6 +518,7 @@ function tryCustomName(seed, id, usedNames, debug) {
|
|||
const validation = validateGeneratedName(customName, { allowAsciiDiagnostic: true });
|
||||
if (!validation.valid) {
|
||||
if (validation.reason === "oneCharacter") debug.oneCharacterNamesPrevented++;
|
||||
if (validation.reason === "oneCharacter") debug.rejectedOneCharacterNames++;
|
||||
else debug.invalidNamesRejected++;
|
||||
return null;
|
||||
}
|
||||
|
|
@ -546,7 +551,10 @@ export function generateEntityName(seed, id, entity, fields, usedNames = null, d
|
|||
continue;
|
||||
}
|
||||
if (result.invalidReason) {
|
||||
if (result.invalidReason === "oneCharacter") debug.oneCharacterNamesPrevented++;
|
||||
if (result.invalidReason === "oneCharacter") {
|
||||
debug.oneCharacterNamesPrevented++;
|
||||
debug.rejectedOneCharacterNames++;
|
||||
}
|
||||
else debug.invalidNamesRejected++;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
515
renderer.js
515
renderer.js
|
|
@ -9,9 +9,9 @@ function distToNearest(points, x, y, fallback = 999) {
|
|||
function blendOutside(color, isInside) {
|
||||
if (isInside) return color;
|
||||
return [
|
||||
Math.round(color[0] * 0.55 + 112),
|
||||
Math.round(color[1] * 0.55 + 112),
|
||||
Math.round(color[2] * 0.55 + 112),
|
||||
Math.round(color[0] * 0.8 + 50),
|
||||
Math.round(color[1] * 0.8 + 50),
|
||||
Math.round(color[2] * 0.8 + 50),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -38,47 +38,36 @@ function terrainColorContinuous(map, fx, fy, mode) {
|
|||
let color;
|
||||
|
||||
if (map.sea[i]) {
|
||||
// Google Map風の海の色(明るい青)
|
||||
const depth = clamp((0.35 - fieldSample(map.elevation, fx, fy)) * 2.4);
|
||||
color = [Math.round(170 + depth * 10), Math.round(211 + depth * 15), Math.round(223 + depth * 20)];
|
||||
color = [Math.round(170 + depth * 5), Math.round(218 + depth * 10), Math.round(255 - depth * 5)];
|
||||
} else if (mode === "suitability") {
|
||||
const a = fieldSample(map.agriculture, fx, fy);
|
||||
const p = fieldSample(map.plain, fx, fy);
|
||||
const f = fieldSample(map.floodplain, fx, fy);
|
||||
color = [
|
||||
Math.round(230 - p * 25 + f * 15),
|
||||
Math.round(235 + a * 15),
|
||||
Math.round(210 - a * 25 + p * 20),
|
||||
Math.round(240 - p * 15 + f * 10),
|
||||
Math.round(242 + a * 10),
|
||||
Math.round(235 - a * 15 + p * 10),
|
||||
];
|
||||
} else if (mode === "development") {
|
||||
const x = Math.floor(fx);
|
||||
const y = Math.floor(fy);
|
||||
const baseIndex = indexOf(Math.max(0, Math.min(MAP_W - 1, x)), Math.max(0, Math.min(MAP_H - 1, y)));
|
||||
const dCity = distToNearest(map.modernCities, fx, fy);
|
||||
const dInd = distToNearest(map.industrialZones, fx, fy);
|
||||
const dLog = distToNearest(map.logisticsParks, fx, fy);
|
||||
const dNew = distToNearest(map.newTowns, fx, fy);
|
||||
const urban = clamp(1 - dCity / 25);
|
||||
const density = map.populationDensity ? fieldSample(map.populationDensity, fx, fy) : urban;
|
||||
const industrial = clamp(1 - dInd / 10);
|
||||
const logistics = clamp(1 - dLog / 9);
|
||||
const newTown = clamp(1 - dNew / 9);
|
||||
const base = 214 + map.plain[baseIndex] * 22;
|
||||
const base = 235;
|
||||
color = [
|
||||
Math.round(base + density * 30 + urban * 8 + industrial * 14),
|
||||
Math.round(base + density * 10 + logistics * 15 + newTown * 12),
|
||||
Math.round(208 + map.agriculture[baseIndex] * 22 + density * 18 + newTown * 22),
|
||||
Math.round(base + density * 20),
|
||||
Math.round(base + density * 5),
|
||||
Math.round(230 + density * 10),
|
||||
];
|
||||
} else {
|
||||
// 起伏の大きさが読めるよう、標高段彩をやや強める
|
||||
// 地形色を少し濃く(暗く)調整
|
||||
const e = fieldSample(map.elevation, fx, fy);
|
||||
const m = fieldSample(map.moisture, fx, fy);
|
||||
if (e > 0.82) color = [178, 170, 160];
|
||||
else if (e > 0.68) color = [198, 188, 164];
|
||||
else if (e > 0.52) color = [205, 222 + m * 5, 184];
|
||||
else if (e > 0.34) color = [224, 238 + m * 6, 206];
|
||||
else if (e > 0.24) color = [236, 245 + m * 5, 220];
|
||||
else color = [218, 232 + m * 6, 206];
|
||||
if (e > 0.82) color = [210, 205, 195];
|
||||
else if (e > 0.68) color = [218, 215, 205];
|
||||
else if (e > 0.52) color = [220, 225, 210];
|
||||
else if (e > 0.34) color = [225, 230, 215];
|
||||
else if (e > 0.24) color = [230, 235, 220];
|
||||
else color = [238, 242, 228];
|
||||
}
|
||||
|
||||
return blendOutside(color, isInside);
|
||||
|
|
@ -89,62 +78,31 @@ function discreteColor(map, x, y, mode) {
|
|||
let color;
|
||||
|
||||
if (map.sea[i]) {
|
||||
// Google Map like styled
|
||||
color = [170, 218, 255];
|
||||
} else if (mode === "landuse") {
|
||||
const colors = {
|
||||
0: [230, 242, 220], // farmland
|
||||
1: [235, 245, 225], // plain
|
||||
2: [235, 230, 220], // old city
|
||||
3: [224, 202, 190], // CBD
|
||||
4: [245, 240, 230], // suburb
|
||||
5: [220, 220, 225], // industrial area
|
||||
6: [225, 235, 225], // logistics area
|
||||
7: [238, 242, 248], // new town
|
||||
8: [248, 242, 230], // coastal development
|
||||
9: [225, 238, 220], // others
|
||||
0: [242, 248, 238],
|
||||
1: [248, 250, 245],
|
||||
2: [240, 238, 232],
|
||||
3: [245, 230, 220],
|
||||
4: [250, 248, 245],
|
||||
5: [235, 235, 240],
|
||||
6: [240, 245, 240],
|
||||
7: [245, 248, 252],
|
||||
8: [250, 248, 240],
|
||||
9: [240, 245, 238],
|
||||
};
|
||||
color = colors[map.landuse[i]] || colors[0];
|
||||
} else if (mode === "admin") {
|
||||
const palette = [
|
||||
[245, 235, 230],
|
||||
[235, 245, 235],
|
||||
[240, 240, 250],
|
||||
[250, 245, 230],
|
||||
[245, 240, 248],
|
||||
[230, 245, 245],
|
||||
[250, 240, 240],
|
||||
[240, 250, 235],
|
||||
[250, 245, 242], [245, 250, 245], [245, 245, 252],
|
||||
[252, 250, 242], [250, 245, 250], [242, 250, 250]
|
||||
];
|
||||
const a = map.adminId[i];
|
||||
color = a >= 0 ? palette[a % palette.length] : [220, 225, 220];
|
||||
} else if (mode === "terrain-debug") {
|
||||
const ridge = clamp(map.ridgeField[i] * 0.68 + (map.arcSpineField?.[i] || 0) * 0.42 + (map.branchRidgeField?.[i] || 0) * 0.34);
|
||||
const deposit = clamp((map.depositionField?.[i] || 0) * 5.0 + (map.depositionalLowland?.[i] || 0) * 0.48 + (map.alluvialFanField?.[i] || 0) * 0.34 + (map.deltaField?.[i] || 0) * 0.46);
|
||||
const valley = clamp(map.valleyField[i] * 0.72 + map.river[i] * 0.22);
|
||||
const barrier = clamp(map.naturalBarrierScore?.[i] || ridge);
|
||||
color = [
|
||||
Math.round(220 - deposit * 70 + ridge * 48),
|
||||
Math.round(226 + deposit * 38 + valley * 28 - barrier * 52),
|
||||
Math.round(214 + valley * 58 + barrier * 34 - ridge * 42),
|
||||
];
|
||||
} else if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
const barrier = clamp(
|
||||
map.ridgeField[i] * 0.88 +
|
||||
Math.max(0, map.river[i] - 0.28) * 1.25 +
|
||||
Math.max(0, map.flowAccum[i] - 0.36) * 0.72 +
|
||||
map.slope[i] * 0.48 +
|
||||
Math.max(0, map.elevation[i] - 0.54) * 0.34
|
||||
);
|
||||
color = [
|
||||
Math.round(238 - barrier * 28),
|
||||
Math.round(242 - barrier * 88),
|
||||
Math.round(226 + barrier * 20),
|
||||
];
|
||||
color = a >= 0 ? palette[a % palette.length] : [240, 242, 240];
|
||||
} else {
|
||||
color = terrainColorContinuous(map, x, y, "terrain");
|
||||
}
|
||||
|
||||
return blendOutside(color, Boolean(map.prefectureMask[i]));
|
||||
}
|
||||
|
||||
|
|
@ -165,17 +123,12 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
|||
const eR = fieldSample(map.elevation, fx + 0.6, fy);
|
||||
const eU = fieldSample(map.elevation, fx, fy - 0.6);
|
||||
const eD = fieldSample(map.elevation, fx, fy + 0.6);
|
||||
const shade = clamp(0.9 + (eR - eL) * 1.0 + (eD - eU) * 0.65, 0.72, 1.18);
|
||||
const elevation = fieldSample(map.elevation, fx, fy);
|
||||
const contour = Math.abs((elevation * 16) - Math.round(elevation * 16));
|
||||
const majorContour = Math.abs((elevation * 8) - Math.round(elevation * 8));
|
||||
const isLand = !map.sea[indexOf(Math.max(0, Math.min(MAP_W - 1, Math.floor(fx))), Math.max(0, Math.min(MAP_H - 1, Math.floor(fy))))];
|
||||
const contourFactor = isLand && majorContour < 0.022 ? 0.86 : isLand && contour < 0.028 ? 0.94 : 1;
|
||||
const shade = clamp(0.95 + (eR - eL) * 0.6 + (eD - eU) * 0.4, 0.85, 1.08);
|
||||
|
||||
const ii = (py * width + px) * 4;
|
||||
img.data[ii] = Math.round(r * shade * contourFactor);
|
||||
img.data[ii + 1] = Math.round(g * shade * contourFactor);
|
||||
img.data[ii + 2] = Math.round(b * shade * contourFactor);
|
||||
img.data[ii] = Math.round(r * shade);
|
||||
img.data[ii + 1] = Math.round(g * shade);
|
||||
img.data[ii + 2] = Math.round(b * shade);
|
||||
img.data[ii + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +148,6 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +158,7 @@ function drawPath(ctx, path, color, width, dashed = false) {
|
|||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
if (dashed) ctx.setLineDash([6, 5]);
|
||||
if (dashed) ctx.setLineDash([8, 6]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
|
|
@ -216,72 +168,64 @@ function drawPath(ctx, path, color, width, dashed = false) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function segmentPointKey(p) {
|
||||
return `${p[0]},${p[1]}`;
|
||||
}
|
||||
// 魚の骨(私鉄記号)スタイルを描画するための専用関数
|
||||
function drawRailway(ctx, path, color, lineWidth, tickLen, spacing) {
|
||||
if (!path || path.length < 2) return;
|
||||
ctx.save();
|
||||
ctx.lineCap = "butt";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
function chainSegments(segments) {
|
||||
const unused = segments.map((seg) => [seg[0], seg[1]]);
|
||||
const chains = [];
|
||||
while (unused.length) {
|
||||
const chain = unused.pop();
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
const head = segmentPointKey(chain[0]);
|
||||
const tail = segmentPointKey(chain[chain.length - 1]);
|
||||
for (let i = unused.length - 1; i >= 0; i--) {
|
||||
const [a, b] = unused[i];
|
||||
const ak = segmentPointKey(a);
|
||||
const bk = segmentPointKey(b);
|
||||
if (ak === tail) { chain.push(b); unused.splice(i, 1); grew = true; break; }
|
||||
if (bk === tail) { chain.push(a); unused.splice(i, 1); grew = true; break; }
|
||||
if (bk === head) { chain.unshift(a); unused.splice(i, 1); grew = true; break; }
|
||||
if (ak === head) { chain.unshift(b); unused.splice(i, 1); grew = true; break; }
|
||||
}
|
||||
}
|
||||
chains.push(chain);
|
||||
// 中心の実線を描画
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
ctx.lineTo(path[k][0] * CELL_SIZE + CELL_SIZE / 2, path[k][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
function chaikin(points, passes = 1) {
|
||||
let out = points;
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
if (out.length < 3) return out;
|
||||
const next = [out[0]];
|
||||
for (let i = 0; i < out.length - 1; i++) {
|
||||
const a = out[i];
|
||||
const b = out[i + 1];
|
||||
next.push([a[0] * 0.75 + b[0] * 0.25, a[1] * 0.75 + b[1] * 0.25]);
|
||||
next.push([a[0] * 0.25 + b[0] * 0.75, a[1] * 0.25 + b[1] * 0.75]);
|
||||
// 棘(クロスハッチ)を描画
|
||||
ctx.lineWidth = 1.0;
|
||||
ctx.beginPath();
|
||||
let leftover = 0;
|
||||
for (let k = 0; k < path.length - 1; k++) {
|
||||
const x1 = path[k][0] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y1 = path[k][1] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const x2 = path[k+1][0] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y2 = path[k+1][1] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist === 0) continue;
|
||||
|
||||
// 法線(直角)ベクトル
|
||||
const nx = dx / dist;
|
||||
const ny = dy / dist;
|
||||
const px = -ny * (tickLen / 2);
|
||||
const py = nx * (tickLen / 2);
|
||||
|
||||
let d = (spacing / 2) + leftover;
|
||||
while (d < dist) {
|
||||
const cx = x1 + nx * d;
|
||||
const cy = y1 + ny * d;
|
||||
ctx.moveTo(cx + px, cy + py);
|
||||
ctx.lineTo(cx - px, cy - py);
|
||||
d += spacing;
|
||||
}
|
||||
next.push(out[out.length - 1]);
|
||||
out = next;
|
||||
leftover = d - dist;
|
||||
}
|
||||
return out;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawSegments(ctx, segments, color, width, dashed = false, smooth = false) {
|
||||
function drawSegments(ctx, segments, color, width, dashed = false) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
if (dashed) ctx.setLineDash([4, 4]);
|
||||
|
||||
if (smooth) {
|
||||
for (const chain of chainSegments(segments)) {
|
||||
const points = chaikin(chain, 1);
|
||||
if (points.length < 2) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0][0] * CELL_SIZE, points[0][1] * CELL_SIZE);
|
||||
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0] * CELL_SIZE, points[i][1] * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
if (dashed) ctx.setLineDash([6, 5]);
|
||||
|
||||
for (const seg of segments) {
|
||||
ctx.beginPath();
|
||||
|
|
@ -296,15 +240,14 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
const visibleModes = ["all", "modern", "development", "landuse", "roads", "admin"];
|
||||
if (!visibleModes.includes(mode)) return;
|
||||
|
||||
// Google Map風の都市部の色
|
||||
const colors = {
|
||||
2: "rgba(235, 230, 220, 0.75)", // 旧市街 - 薄いベージュ
|
||||
3: "rgba(224, 202, 190, 0.9)", // 中心市街地 / CBD - cell fill
|
||||
4: "rgba(245, 242, 235, 0.7)", // 郊外 - 薄いクリーム
|
||||
5: "rgba(220, 220, 228, 0.8)", // 工業地域 - 薄いグレー
|
||||
6: "rgba(225, 235, 228, 0.75)", // 物流 - 薄い緑グレー
|
||||
7: "rgba(238, 242, 250, 0.75)", // ニュータウン - 薄い青白
|
||||
8: "rgba(248, 245, 238, 0.7)", // 沿道 - クリーム
|
||||
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)",
|
||||
};
|
||||
|
||||
ctx.save();
|
||||
|
|
@ -319,38 +262,22 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
const py = y * CELL_SIZE;
|
||||
ctx.fillStyle = colors[lu];
|
||||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const h = ((x * 92821 + y * 68917 + lu * 131) >>> 0);
|
||||
if (lu === 3 || lu === 2 || lu === 5 || lu === 6) {
|
||||
// 建物の表現を控えめに
|
||||
ctx.fillStyle = lu === 3 ? "rgba(200,200,200,0.25)" : "rgba(210,210,210,0.2)";
|
||||
if (h % 3 !== 0) ctx.fillRect(px + 1, py + 1, 2, 2);
|
||||
if (h % 5 !== 0) ctx.fillRect(px + 3, py + 2, 2, 2);
|
||||
if (h % 7 !== 0) ctx.fillRect(px + 2, py + 4, 2, 1.5);
|
||||
} else if (lu === 4 || lu === 7) {
|
||||
ctx.strokeStyle = lu === 7 ? "rgba(220,220,230,0.15)" : "rgba(200,190,180,0.15)";
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.beginPath();
|
||||
if (h % 2 === 0) {
|
||||
ctx.moveTo(px + 1, py + 1);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 1);
|
||||
ctx.moveTo(px + 1, py + 4);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 4);
|
||||
} else {
|
||||
ctx.moveTo(px + 1, py + 1);
|
||||
ctx.lineTo(px + 1, py + CELL_SIZE - 1);
|
||||
ctx.moveTo(px + 4, py + 1);
|
||||
ctx.lineTo(px + 4, py + CELL_SIZE - 1);
|
||||
}
|
||||
ctx.stroke();
|
||||
} else if (lu === 8) {
|
||||
ctx.strokeStyle = "rgba(180,170,160,0.2)";
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + 1, py + 3);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 3);
|
||||
ctx.stroke();
|
||||
}
|
||||
function drawDebugCells(ctx, map, field, color) {
|
||||
if (!field) return;
|
||||
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] || map.sea[i]) continue;
|
||||
const v = clamp(field[i] || 0, 0, 1);
|
||||
if (v <= 0.12) continue;
|
||||
ctx.fillStyle = color(v);
|
||||
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
|
@ -361,91 +288,41 @@ function dot(ctx, p, radius, fill, stroke = "white") {
|
|||
ctx.arc(p.x * CELL_SIZE + CELL_SIZE / 2, p.y * CELL_SIZE + CELL_SIZE / 2, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function squareIcon(ctx, p, size, fill, stroke = "white") {
|
||||
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.rect(x - size / 2, y - size / 2, size, size);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function triangleIcon(ctx, p, size, fill, stroke = "white") {
|
||||
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y - size / 2);
|
||||
ctx.lineTo(x + size / 2, y + size / 2);
|
||||
ctx.lineTo(x - size / 2, y + size / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function railStationIcon(ctx, p) {
|
||||
squareIcon(ctx, p, 5.2, "rgba(255,255,255,0.96)", "rgba(55,55,55,0.92)");
|
||||
}
|
||||
|
||||
|
||||
function drawHarborWorks(ctx, map) {
|
||||
if (!map.harborWorks) return;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(95, 120, 150, 0.9)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.lineCap = "round";
|
||||
for (const harbor of map.harborWorks) {
|
||||
for (const seg of harbor.segments || []) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(seg[0][0] * CELL_SIZE + CELL_SIZE / 2, seg[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
ctx.lineTo(seg[1][0] * CELL_SIZE + CELL_SIZE / 2, seg[1][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function boxesOverlap(a, b, pad = 2) {
|
||||
function boxesOverlap(a, b, pad = 3) {
|
||||
return !(a.x2 + pad < b.x1 || a.x1 - pad > b.x2 || a.y2 + pad < b.y1 || a.y1 - pad > b.y2);
|
||||
}
|
||||
|
||||
function labelWithCollision(ctx, p, occupied) {
|
||||
if (!p.name) return false;
|
||||
ctx.save();
|
||||
ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
|
||||
ctx.font = "600 11.5px ui-sans-serif, system-ui, -apple-system, sans-serif";
|
||||
const baseX = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const baseY = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
const textW = ctx.measureText(p.name).width;
|
||||
const textH = 12;
|
||||
const candidates = [
|
||||
[7, -5], [7, 12], [-textW - 7, -5], [-textW - 7, 12],
|
||||
[-textW / 2, -13], [-textW / 2, 20], [12, 2], [-textW - 12, 2],
|
||||
[7, -4], [7, 13], [-textW - 7, -4], [-textW - 7, 13],
|
||||
[-textW / 2, -12], [-textW / 2, 20], [12, 4], [-textW - 12, 4],
|
||||
];
|
||||
|
||||
for (const [ox, oy] of candidates) {
|
||||
const x = baseX + ox;
|
||||
const y = baseY + oy;
|
||||
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 3 };
|
||||
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 4 };
|
||||
if (box.x1 < 0 || box.y1 < 0 || box.x2 > MAP_W * CELL_SIZE || box.y2 > MAP_H * CELL_SIZE) continue;
|
||||
if (occupied.some((b) => boxesOverlap(box, b))) continue;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.fillStyle = "rgba(40,40,40,0.95)";
|
||||
|
||||
ctx.lineJoin = "round";
|
||||
ctx.lineWidth = 3.5;
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
|
||||
ctx.strokeText(p.name, x, y);
|
||||
|
||||
ctx.fillStyle = p.isPrefecturalCapital ? "#111111" : "#333333";
|
||||
ctx.fillText(p.name, x, y);
|
||||
occupied.push(box);
|
||||
ctx.restore();
|
||||
|
|
@ -459,7 +336,7 @@ function drawLabels(ctx, points, limit = Infinity) {
|
|||
const occupied = [];
|
||||
const prioritized = points
|
||||
.filter((p) => p?.name)
|
||||
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) + (p.kind === "Market Town" ? 48 : 0) + (p.kind?.includes("Castle") ? 70 : 0) + (p.kind === "External Gateway" ? 60 : 0) + (p.kind === "Municipal Center" ? 34 : 0) }))
|
||||
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) }))
|
||||
.sort((a, b) => b.labelPriority - a.labelPriority);
|
||||
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
|
||||
}
|
||||
|
|
@ -471,111 +348,113 @@ export function drawMap(canvas, map, options) {
|
|||
const mode = options.mode || "all";
|
||||
const showFeatures = options.showFeatures !== false;
|
||||
const showLabels = options.showLabels !== false;
|
||||
const continuousTerrain = true;
|
||||
|
||||
|
||||
const width = MAP_W * CELL_SIZE;
|
||||
const height = MAP_H * CELL_SIZE;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
drawBase(ctx, map, mode, continuousTerrain);
|
||||
// 1. Base Terrain & Urban
|
||||
drawBase(ctx, map, mode, true);
|
||||
drawUrbanAreas(ctx, map, mode);
|
||||
|
||||
// Rivers use the same hue family as the sea; hierarchy is expressed by width/opacity.
|
||||
const waterBlue = "rgba(170, 218, 255, 0.95)";
|
||||
// Small streams remain in the data model but are not drawn by default.
|
||||
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(170, 218, 255, 0.78)", 1.35);
|
||||
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.2);
|
||||
drawHarborWorks(ctx, map);
|
||||
// 2. Rivers
|
||||
const waterBlue = "rgba(160, 205, 240, 1)";
|
||||
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(160, 205, 240, 0.8)", 1.5);
|
||||
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.5);
|
||||
|
||||
const debugBorders = mode === "admin-debug" || mode === "borders-debug";
|
||||
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, debugBorders ? "rgba(40,40,40,0.82)" : mode === "all" ? "rgba(95,95,95,0.18)" : "rgba(95,95,95,0.30)", debugBorders ? 1.8 : 1.0, false, mode === "all");
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(30,30,30,0.82)", 2.4, false, true);
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(255,255,255,0.74)", 1.05, false, true);
|
||||
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 showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
|
||||
|
||||
// 3. Borders
|
||||
if (showAdmin && map.adminBorders) {
|
||||
drawSegments(ctx, map.adminBorders, "rgba(255, 255, 255, 0.8)", 2.8, false);
|
||||
drawSegments(ctx, map.adminBorders, "rgba(150, 140, 150, 0.9)", 1.2, true);
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
drawDebugCells(ctx, map, map.naturalBarrierScore, (v) => `rgba(255, 120, 40, ${0.06 + v * 0.18})`);
|
||||
if (map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(60, 110, 170, 0.42)", 0.8, true);
|
||||
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, "rgba(70, 55, 95, 0.95)", 2.4, false);
|
||||
}
|
||||
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(255, 255, 255, 0.95)", 5.0, false);
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(110, 90, 110, 1)", 2.2, true);
|
||||
|
||||
if (!showFeatures) return;
|
||||
|
||||
const showHistory = ["history", "all", "terrain", "suitability"].includes(mode);
|
||||
const showModern = ["modern", "all", "development", "landuse"].includes(mode);
|
||||
const showRoads = ["roads", "all", "development", "landuse"].includes(mode);
|
||||
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
|
||||
|
||||
if (debugBorders && map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(70,70,70,0.32)", 0.75);
|
||||
if (showAdmin) drawSegments(ctx, map.adminBorders, debugBorders ? "rgba(20,90,180,0.96)" : mode === "all" ? "rgba(120,120,120,0.28)" : "rgba(120,120,120,0.65)", debugBorders ? 2.1 : mode === "all" ? 0.9 : 1.3);
|
||||
if (showAdmin && mode !== "all") {
|
||||
for (const p of map.adminCenters || []) dot(ctx, p, 3.0, "rgba(255,255,255,0.96)", "rgba(70,90,120,0.85)");
|
||||
}
|
||||
|
||||
// 4. Casings (Outlines)
|
||||
if (showHistory) {
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, mode === "all" ? "rgba(150, 120, 90, 0.34)" : "rgba(150, 120, 90, 0.55)", mode === "all" ? 1.15 : 1.45, true);
|
||||
for (const path of map.minorRoads) drawPath(ctx, path, mode === "all" ? "rgba(180, 150, 120, 0.28)" : "rgba(180, 150, 120, 0.62)", mode === "all" ? 0.8 : 1.05);
|
||||
// 古い道は白の実線が引き立つように淡いケーシングを敷く
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(210, 210, 210, 0.7)", 2.5);
|
||||
}
|
||||
|
||||
if (showModern) {
|
||||
// 鉄道 - 濃いグレー
|
||||
for (const path of map.railways) drawPath(ctx, path, "rgba(80, 80, 80, 0.9)", 2.8);
|
||||
for (const path of map.ringRailways || []) drawPath(ctx, path, "rgba(70, 70, 70, 0.82)", 2.1);
|
||||
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(100, 100, 100, 0.82)", 1.9);
|
||||
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(90, 90, 90, 0.9)", 2.4);
|
||||
}
|
||||
|
||||
if (showRoads) {
|
||||
// Google Map風の道路 - 白と黄色とオレンジ
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.2);
|
||||
for (const path of map.icAccessRoads || []) drawPath(ctx, path, "rgba(255, 230, 150, 0.82)", 1.45);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.0);
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.0);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.5);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.2);
|
||||
}
|
||||
|
||||
if (showHistory) {
|
||||
for (const p of map.villages) dot(ctx, p, mode === "all" ? 1.35 : 1.9, mode === "all" ? "rgba(120, 100, 80, 0.42)" : "rgba(120, 100, 80, 0.72)");
|
||||
for (const p of map.markets) dot(ctx, p, 4.8, "rgba(200, 130, 80, 0.95)");
|
||||
for (const p of map.ports) {
|
||||
const color = p.portClass === "major" ? "rgba(40, 105, 190, 0.98)" : p.portClass === "regional" ? "rgba(70, 130, 200, 0.95)" : p.portClass === "lake" ? "rgba(80, 155, 180, 0.92)" : "rgba(95, 150, 195, 0.82)";
|
||||
triangleIcon(ctx, p, p.portClass === "major" ? 8.2 : 6.5, color);
|
||||
if (showModern || showRoads) {
|
||||
// 鉄道のケーシング(白背景を敷いて視認性を保つ)
|
||||
for (const path of map.railways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
|
||||
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.6)", 2.5);
|
||||
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
|
||||
|
||||
// 幹線道路のケーシング(色を濃く)
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
}
|
||||
for (const p of map.crossings) dot(ctx, p, 3.5, "rgba(255, 240, 180, 0.95)", "rgba(100,90,70,0.8)");
|
||||
for (const p of map.passes) dot(ctx, p, 3.9, "rgba(150, 120, 180, 0.95)");
|
||||
for (const p of map.castles) squareIcon(ctx, p, 7.0, "rgba(180, 70, 70, 0.96)");
|
||||
for (const p of map.castleRuins) dot(ctx, p, 3.2, "rgba(110, 90, 90, 0.9)", "rgba(220,220,220,0.8)");
|
||||
}
|
||||
|
||||
// 5. Fills (Inner colors) & Fishbones
|
||||
if (showHistory) {
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(255, 255, 255, 1)", 1.2, false);
|
||||
}
|
||||
|
||||
if (showModern || showRoads) {
|
||||
// 鉄道の骨線描画(色, 線幅, 棘の長さ, 棘の間隔)
|
||||
for (const path of map.railways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
|
||||
for (const path of map.branchRailways) drawRailway(ctx, path, "rgba(140, 140, 140, 1)", 1.0, 4.0, 6.0);
|
||||
for (const path of map.externalRailways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
|
||||
|
||||
// 幹線道路の塗り(色を濃く)
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Icons & Labels
|
||||
if (showModern) {
|
||||
for (const p of map.industrialZones) squareIcon(ctx, p, 6.5, "rgba(140, 140, 150, 0.96)");
|
||||
for (const p of map.stations) railStationIcon(ctx, p);
|
||||
for (const p of map.satelliteCities || []) dot(ctx, p, 4.8, "rgba(215, 95, 145, 0.95)");
|
||||
for (const p of map.newTowns) triangleIcon(ctx, p, 6.5, "rgba(180, 210, 240, 0.95)");
|
||||
for (const p of map.stations) dot(ctx, p, 2.5, "#fff", "#444");
|
||||
for (const p of map.modernCities) {
|
||||
const popRadius = p.population ? Math.min(9.2, 3.4 + Math.sqrt(p.population) / 360) : 4.7;
|
||||
const rankBoost = p.isPrefecturalCapital || p.rank === "Prefectural Capital" ? 1.6 : p.rank === "Regional Center" ? 0.45 : 0;
|
||||
dot(ctx, p, popRadius + rankBoost, "rgba(230, 100, 100, 0.95)");
|
||||
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 4.2, "rgba(255,255,255,0.0)", "rgba(180,60,60,0.95)");
|
||||
const popRadius = p.population ? Math.min(8.5, 3.5 + Math.sqrt(p.population) / 400) : 4.5;
|
||||
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)");
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
for (const p of map.externalGateways || []) dot(ctx, p, 5.5, "rgba(255,255,255,0.95)", "rgba(40,80,170,0.95)");
|
||||
for (const p of map.transportDebug?.missingRequiredNodes || []) dot(ctx, p, 6.5, "rgba(255,80,80,0.95)", "rgba(80,20,20,0.95)");
|
||||
}
|
||||
}
|
||||
|
||||
if (showRoads) {
|
||||
for (const p of map.logisticsParks) squareIcon(ctx, p, 6.2, "rgba(110, 170, 140, 0.96)");
|
||||
for (const p of map.interchanges) dot(ctx, p, 4.1, "rgba(255, 255, 255, 0.98)", "rgba(220, 90, 60, 0.95)");
|
||||
for (const p of map.externalGateways) dot(ctx, p, 4.4, "rgba(255, 250, 200, 0.98)", "rgba(60,60,60,0.92)");
|
||||
}
|
||||
|
||||
if (showLabels) {
|
||||
const adminLabels = (map.adminCenters || [])
|
||||
.map((p) => ({ ...p, labelPriorityBase: mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? 90 : 8 }))
|
||||
.filter((p) => mode !== "all" || p.representativeFeatureName);
|
||||
if (mode === "admin") {
|
||||
drawLabels(ctx, map.adminCenters || [], Infinity);
|
||||
return;
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
drawLabels(ctx, [...(map.adminCenters || []), ...(map.externalGateways || [])], Infinity);
|
||||
return;
|
||||
}
|
||||
const important = [
|
||||
...map.modernCities,
|
||||
...map.ports,
|
||||
...map.markets.slice(0, mode === "all" ? 6 : 10),
|
||||
...map.castles.slice(0, mode === "all" ? 5 : 8),
|
||||
...(map.satelliteCities || []),
|
||||
...map.newTowns,
|
||||
...(mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? adminLabels : adminLabels.slice(0, 8)),
|
||||
...map.externalGateways,
|
||||
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
|
||||
|
||||
drawLabels(ctx, important, mode === "all" ? 34 : Infinity);
|
||||
drawLabels(ctx, important, 60);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
styles.css
67
styles.css
|
|
@ -1,11 +1,68 @@
|
|||
*{box-sizing:border-box} body{margin:0;background:#f5f5f5;color:#2c2c2c;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} button,input{font:inherit} code{background:#e8e8e8;border-radius:4px;padding:1px 4px} .app{min-height:100vh;padding:24px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px;max-width:1400px;margin:0 auto}.header{margin-bottom:16px}.header h1{margin:0 0 6px;font-size:24px;letter-spacing:-0.02em;color:#1a1a1a}.header p{margin:0;color:#5a5a5a;line-height:1.65;font-size:14px}.canvas-shell,.card{background:#ffffff;border:1px solid rgb(0 0 0 / 0.12);border-radius:18px;box-shadow:0 2px 8px rgb(0 0 0 / 0.08)}.canvas-shell{padding:12px;overflow:auto;position:relative}.map-canvas{display:block;border-radius:12px;background:#f8f8f8}.sidebar{display:flex;flex-direction:column;gap:14px}.card{padding:16px}.label,.card-title{display:block;margin-bottom:10px;color:#2c2c2c;font-size:14px;font-weight:650}.input{width:100%;border:1px solid rgb(0 0 0 / 0.18);background:#fafafa;color:#2c2c2c;border-radius:12px;padding:9px 11px;outline:none}.input:focus{border-color:rgb(66 133 244 / 0.6)}.primary-button,.mode-button{border:0;border-radius:12px;padding:9px 11px;cursor:pointer}.primary-button{margin-top:10px;width:100%;background:#1a73e8;color:#fff;font-weight:650}.primary-button:hover{background:#1557b0}.mode-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.mode-button{background:#f1f3f4;color:#3c4043;border:1px solid transparent}.mode-button:hover{background:#e8eaed}.mode-button.active{background:#1a73e8;color:#fff;font-weight:650;border:1px solid #1a73e8}.checkbox-row{display:flex;gap:8px;align-items:center;margin-top:12px;color:#3c4043;font-size:14px}.stats{display:flex;flex-direction:column;gap:7px}.stat-row{display:flex;justify-content:space-between;gap:12px;color:#5f6368;font-size:13px;align-items:baseline}.stat-row strong{color:#202124;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.legend{color:#5f6368;font-size:12px;line-height:1.65}.legend p{margin:8px 0 0}.example{margin:10px 0;padding:10px;background:#f8f9fa;border:1px solid rgb(0 0 0 / 0.1);border-radius:10px;color:#3c4043;overflow:auto}.id-list{max-height:220px;overflow:auto;margin-top:10px;display:flex;flex-direction:column;gap:6px}.id-row{display:grid;grid-template-columns:112px 1fr;gap:8px;align-items:center;color:#5f6368;font-size:12px}@media (max-width:1100px){.layout{grid-template-columns:1fr}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:#f0f2f5;color:#2c2c2c;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
button,input{font:inherit}
|
||||
code{background:#e8e8e8;border-radius:4px;padding:1px 4px}
|
||||
.app{min-height:100vh;padding:24px}
|
||||
.layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:20px;max-width:1400px;margin:0 auto}
|
||||
.header{margin-bottom:16px}
|
||||
.header h1{margin:0 0 6px;font-size:24px;letter-spacing:-0.02em;color:#1a1a1a;font-weight:700}
|
||||
.header p{margin:0;color:#5a5a5a;line-height:1.6;font-size:14px}
|
||||
.canvas-shell,.card{background:#ffffff;border:1px solid rgba(0,0,0,0.08);border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.04)}
|
||||
.canvas-shell{padding:12px;overflow:auto;position:relative}
|
||||
.map-canvas{display:block;border-radius:8px;background:#f8f9fa}
|
||||
.sidebar{display:flex;flex-direction:column;gap:16px}
|
||||
.card{padding:18px}
|
||||
.label,.card-title{display:block;margin-bottom:12px;color:#1a1a1a;font-size:14px;font-weight:600}
|
||||
.input{width:100%;border:1px solid rgba(0,0,0,0.15);background:#fff;color:#2c2c2c;border-radius:8px;padding:10px 12px;outline:none;transition:border-color 0.2s}
|
||||
.input:focus{border-color:#1a73e8;box-shadow:0 0 0 3px rgba(26,115,232,0.15)}
|
||||
.primary-button,.mode-button{border:0;border-radius:8px;padding:10px 12px;cursor:pointer;font-weight:600;transition:background 0.2s}
|
||||
.primary-button{margin-top:12px;width:100%;background:#1a73e8;color:#fff}
|
||||
.primary-button:hover{background:#1557b0}
|
||||
.mode-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.mode-button{background:#f1f3f4;color:#3c4043;border:1px solid transparent}
|
||||
.mode-button:hover{background:#e8eaed}
|
||||
.mode-button.active{background:#e8f0fe;color:#1a73e8;border:1px solid #1a73e8}
|
||||
.checkbox-row{display:flex;gap:8px;align-items:center;margin-top:12px;color:#3c4043;font-size:14px;cursor:pointer}
|
||||
.stats{display:flex;flex-direction:column;gap:8px}
|
||||
.stat-row{display:flex;justify-content:space-between;gap:12px;color:#5f6368;font-size:13px;align-items:baseline}
|
||||
.stat-row strong{color:#202124;font-family:ui-monospace,monospace}
|
||||
.legend{color:#5f6368;font-size:13px;line-height:1.6}
|
||||
.legend p{margin:8px 0 0}
|
||||
.example{margin:10px 0;padding:12px;background:#f8f9fa;border:1px solid rgba(0,0,0,0.08);border-radius:8px;color:#3c4043;overflow:auto;font-size:12px}
|
||||
.id-list{max-height:220px;overflow:auto;margin-top:12px;display:flex;flex-direction:column;gap:6px}
|
||||
.id-row{display:grid;grid-template-columns:112px 1fr;gap:8px;align-items:center;color:#5f6368;font-size:12px}
|
||||
|
||||
.legend-grid{display:flex;flex-direction:column;gap:7px;margin-top:8px}.legend-row{display:grid;grid-template-columns:30px 1fr;gap:8px;align-items:center;min-height:20px}.legend-line{display:inline-block;width:28px;height:0;border-top:3px solid #777;border-radius:999px}.legend-swatch{display:inline-block;width:26px;height:14px;border-radius:5px;background:#f5f5f5}.border-swatch{border:2px solid rgba(80,80,80,.8);box-shadow:inset 0 0 0 1px rgba(255,255,255,.9)}.river-major{border-top:4px solid rgba(100,170,210,.95);box-shadow:0 3px 0 rgba(120,180,215,.55)}.rail-line{border-top:3px solid rgba(70,70,70,.95)}.road-line{border-top:3px solid rgba(252,210,90,.95)}.express-line{border-top:5px solid rgba(245,140,60,.95)}.old-road-line{border-top:2px dashed rgba(150,120,90,.75)}.legend-icon{display:inline-block;width:15px;height:15px;justify-self:center;border:2px solid #fff;box-shadow:0 0 0 1px rgb(0 0 0 / .28)}.city-icon{border-radius:50%;background:rgba(230,100,100,.95);width:17px;height:17px}.port-icon{width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-bottom:15px solid rgba(70,130,200,.95);border-top:0;box-shadow:none;background:transparent}.castle-icon{background:rgba(180,70,70,.96);border-radius:2px}.station-icon{background:#fff;border-color:rgba(60,60,60,.9);border-radius:2px}.industry-icon{background:rgba(110,170,140,.96);border-radius:2px}.newtown-icon{width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-bottom:15px solid rgba(180,210,240,.95);border-top:0;box-shadow:none;background:transparent}
|
||||
@media (max-width:1100px){.layout{grid-template-columns:1fr}}
|
||||
|
||||
.legend-grid{display:flex;flex-direction:column;gap:8px;margin-top:12px}
|
||||
.legend-row{display:grid;grid-template-columns:32px 1fr;gap:8px;align-items:center;min-height:22px}
|
||||
|
||||
/* Layered GIS style CSS equivalents */
|
||||
.legend-line{display:inline-block;width:28px;height:4px;border-radius:2px;}
|
||||
.express-line{background:#6eb982; border:1px solid #508c64;}
|
||||
.road-line{background:#f5e182; border:1px solid #beaf8c;}
|
||||
|
||||
.legend-swatch{width:18px;height:14px;border-radius:4px;border:1px solid rgb(0 0 0 / 0.18);display:inline-block}.cbd-swatch{background:rgb(224 202 190)}.satellite-icon{background:rgba(215,95,145,0.95);border-radius:999px;border:2px solid #fff}
|
||||
/* Fishbone Railway Style */
|
||||
.rail-line{background:#6e6e6e; height:1.5px; position:relative; border:none; margin-top:2px; border-radius:0}
|
||||
.rail-line::after{content:"";position:absolute;top:-2.5px;left:0;right:0;height:7px;background:repeating-linear-gradient(90deg, transparent, transparent 5px, #6e6e6e 5px, #6e6e6e 6px);}
|
||||
|
||||
.legend-line.harbor-line::before{background:#6b86a0;height:3px;top:7px}
|
||||
.river-major{background:#a0cdf0; border:none; height:3px;}
|
||||
.old-road-line{background:#fff; border:1px solid #dcdcdc; height:3px; border-top:none;}
|
||||
|
||||
.map-tooltip{position:absolute;z-index:20;pointer-events:none;min-width:190px;max-width:270px;background:rgba(255,255,255,.96);border:1px solid rgb(0 0 0 / .16);border-radius:10px;box-shadow:0 8px 24px rgb(0 0 0 / .16);padding:8px 10px;color:#2c2c2c;font-size:12px;line-height:1.5;opacity:0;transform:translateY(2px);transition:opacity .08s ease,transform .08s ease}.map-tooltip.visible{opacity:1;transform:translateY(0)}
|
||||
.legend-swatch{display:inline-block;width:24px;height:14px;border-radius:4px;background:#f5f5f5}
|
||||
.border-swatch{border:2px dashed rgba(110,90,110,1);box-shadow:inset 0 0 0 1px rgba(255,255,255,1), 0 0 0 1px rgba(255,255,255,1)}
|
||||
.cbd-swatch{background:#f0dccd; border:1px solid rgba(0,0,0,0.1)}
|
||||
|
||||
.legend-icon{display:inline-block;width:14px;height:14px;justify-self:center;border:2px solid #fff;border-radius:50%;box-shadow:0 0 0 1px rgba(0,0,0,0.15)}
|
||||
.city-icon{background:#f06e6e;}
|
||||
.satellite-icon{background:#d75f91;}
|
||||
.station-icon{background:#fff;border-color:#444;}
|
||||
.industry-icon{background:#8caaa0; border-radius:3px;}
|
||||
.castle-icon{background:#b44646; border-radius:3px;}
|
||||
|
||||
.port-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #4682c8;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
||||
.newtown-icon{width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:14px solid #b4d2f0;border-top:0;box-shadow:none;background:transparent;border-radius:0}
|
||||
.legend-line.harbor-line{background:transparent; border-top:2px solid #5f7896; height:0}
|
||||
|
||||
.map-tooltip{position:absolute;z-index:20;pointer-events:none;min-width:200px;max-width:280px;background:rgba(255,255,255,0.98);border:1px solid rgba(0,0,0,0.1);border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.1);padding:10px 12px;color:#2c2c2c;font-size:12px;line-height:1.5;opacity:0;transform:translateY(4px);transition:opacity 0.15s ease, transform 0.15s ease;font-weight:500}
|
||||
.map-tooltip.visible{opacity:1;transform:translateY(0)}
|
||||
125
test.js
125
test.js
|
|
@ -284,6 +284,97 @@ function terrainCoreMetrics(map) {
|
|||
};
|
||||
}
|
||||
|
||||
function requiredTransportNodes(map) {
|
||||
const nodes = [];
|
||||
const seen = new Set();
|
||||
function add(p, reason) {
|
||||
if (!p) return;
|
||||
const key = `${p.x},${p.y}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
nodes.push({ ...p, requiredTransportReason: reason });
|
||||
}
|
||||
add(map.prefecturalCapital, "capital");
|
||||
for (const gate of map.externalGateways || []) add(gate, "externalGateway");
|
||||
for (const city of map.modernCities || []) if ((city.population || 0) >= 120000 || city.isPrefecturalCapital) add(city, "majorCity");
|
||||
for (const port of map.ports || []) if (port.portClass === "major") add(port, "majorPort");
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function transportConnectivityMetrics(map) {
|
||||
const paths = [
|
||||
...map.railways,
|
||||
...map.branchRailways,
|
||||
...map.externalRailways,
|
||||
...map.nationalRoads,
|
||||
...(map.ringRoads || []),
|
||||
...map.expressways,
|
||||
...map.externalRoads,
|
||||
...map.externalExpressways,
|
||||
];
|
||||
const pathCells = new Set();
|
||||
for (const path of paths) for (const [x, y] of path) pathCells.add(`${x},${y}`);
|
||||
const nodes = requiredTransportNodes(map);
|
||||
function nearestCell(node) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
for (const key of pathCells) {
|
||||
const [x, y] = key.split(",").map(Number);
|
||||
const d = Math.hypot(node.x - x, node.y - y);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = key;
|
||||
}
|
||||
}
|
||||
return { key: best, distance: bestD };
|
||||
}
|
||||
const seen = new Set();
|
||||
const components = [];
|
||||
for (const key of pathCells) {
|
||||
if (seen.has(key)) continue;
|
||||
const queue = [key];
|
||||
const component = new Set([key]);
|
||||
seen.add(key);
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const [x, y] = queue[q].split(",").map(Number);
|
||||
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]) {
|
||||
const nk = `${x + dx},${y + dy}`;
|
||||
if (!pathCells.has(nk) || seen.has(nk)) continue;
|
||||
seen.add(nk);
|
||||
component.add(nk);
|
||||
queue.push(nk);
|
||||
}
|
||||
}
|
||||
components.push(component);
|
||||
}
|
||||
const mapped = nodes.map((node) => ({
|
||||
node,
|
||||
nearest: nearestCell(node),
|
||||
components: components
|
||||
.map((component, componentIndex) => ({
|
||||
componentIndex,
|
||||
near: [...component].some((key) => {
|
||||
const [x, y] = key.split(",").map(Number);
|
||||
return Math.hypot(node.x - x, node.y - y) <= 7;
|
||||
}),
|
||||
}))
|
||||
.filter((item) => item.near)
|
||||
.map((item) => item.componentIndex),
|
||||
}));
|
||||
const reachable = mapped.filter((item) => item.components.length > 0);
|
||||
let largestRequiredComponent = 0;
|
||||
for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
|
||||
largestRequiredComponent = Math.max(largestRequiredComponent, reachable.filter((item) => item.components.includes(componentIndex)).length);
|
||||
}
|
||||
return {
|
||||
requiredCount: nodes.length,
|
||||
reachableCount: reachable.length,
|
||||
largestRequiredComponent,
|
||||
isolatedExternalGateways: mapped.filter((item) => item.node.requiredTransportReason === "externalGateway" && item.components.length === 0).length,
|
||||
isolatedMajorCities: mapped.filter((item) => item.node.requiredTransportReason === "majorCity" && item.components.length === 0).length,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const map = generateMap(12345);
|
||||
const other = generateMap(54321);
|
||||
|
|
@ -401,6 +492,7 @@ try {
|
|||
const satelliteMetrics = satelliteMunicipalityMetrics(map);
|
||||
const regionalMetrics = regionalComponentMetrics(map);
|
||||
const terrainMetrics = terrainCoreMetrics(map);
|
||||
const transportMetrics = transportConnectivityMetrics(map);
|
||||
|
||||
assert(NAME_KANJI_POOLS && Array.isArray(NAME_KANJI_POOLS.modifiers), "NAME_KANJI_POOLS exists");
|
||||
assert(NAME_TEMPLATES && NAME_TEMPLATES.modifierTerrain?.slots?.length === 2, "NAME_TEMPLATES exists");
|
||||
|
|
@ -430,6 +522,11 @@ try {
|
|||
assert(map.depositionalLowland.length === size && map.alluvialFanField.length === size && map.deltaField.length === size, "depositional debug fields match map size");
|
||||
assert(map.naturalBarrierScore.length === size, "natural barrier score field matches map size");
|
||||
assert(map.terrainTemplate && Number.isFinite(map.terrainTemplate.deposition) && Number.isFinite(map.terrainTemplate.erosion), "terrain template parameters are exposed");
|
||||
assert(map.terrainDebug && Number.isFinite(map.terrainDebug.primarySpineStrength), "terrain debug metrics exist");
|
||||
assert(map.terrainDebug.primarySpineStrength > 0.08, "primary mountain spine has visible strength");
|
||||
assert(map.terrainDebug.largeInlandLakeCount <= 1, "large inland lakes are rare");
|
||||
assert(map.terrainDebug.smallIslandCount <= 24, "small island/coast speckles stay limited");
|
||||
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");
|
||||
|
|
@ -439,6 +536,8 @@ try {
|
|||
assert(map.regionalDebug.regionalBorderCountBefore > 0 && map.regionalDebug.regionalBorderCountAfter > 0, "regional border counts are tracked");
|
||||
assert(map.regionalDebug.regionalNaturalBarrierAverageAfter >= map.regionalDebug.regionalNaturalBarrierAverageBefore - 0.08, "regional border natural-barrier affinity does not degrade meaningfully");
|
||||
assert(map.regionalDebug.regionalVoronoiLikeRateAfter <= map.regionalDebug.regionalVoronoiLikeRateBefore + 0.22, "regional weak Voronoi-like border rate stays bounded");
|
||||
assert(map.regionalDebug.compartmentCount > 0 && map.regionalDebug.changedAfterCompartmentAssignment > 0, "regional compartment assignment debug is available");
|
||||
assert(Number.isFinite(map.regionalDebug.borderNaturalBarrierAverage) && Number.isFinite(map.regionalDebug.voronoiLikeRate), "regional natural-border aliases are exposed");
|
||||
assert(regionalMetrics.regionCount >= 4 && regionalMetrics.maxComponents <= 5, "regional prefecture regions remain connected enough for display");
|
||||
assert(Array.isArray(map.tributaryRivers) && Array.isArray(map.smallStreams), "river hierarchy arrays exist");
|
||||
assert(Array.isArray(map.icAccessRoads), "IC access road array exists");
|
||||
|
|
@ -495,8 +594,12 @@ try {
|
|||
assert(map.ports.some((p) => p.portClass === "major"), "at least one major port is classified");
|
||||
assert(map.ports.every((p) => ["major", "regional", "fishing", "lake"].includes(p.portClass)), "ports have explicit classes");
|
||||
assert(map.externalGateways.length > 0, "external gateways exist");
|
||||
assert(map.transportDebug && map.transportDebug.requiredNodeCount > 0, "transport required-node debug exists");
|
||||
assert(transportMetrics.requiredCount > 0 && transportMetrics.reachableCount === transportMetrics.requiredCount, "required transport nodes touch the modern network");
|
||||
assert(transportMetrics.largestRequiredComponent === transportMetrics.requiredCount, "required transport nodes are in one connected modern component");
|
||||
assert(transportMetrics.isolatedExternalGateways === 0 && transportMetrics.isolatedMajorCities === 0, "external gateways and major cities are not isolated");
|
||||
assert(map.minorRoads.length > 0, "minor roads exist");
|
||||
assert(map.adminCenters.length >= 12, "municipality count is sufficiently large");
|
||||
assert(map.adminCenters.length >= 18, "municipality center count is sufficiently large");
|
||||
assert(adminMetrics.municipalityCount >= 10, "terrain snapping preserves a reasonable municipality count");
|
||||
assert(adminMetrics.centerValidRatio >= 0.95, "municipality centers remain on valid assigned land cells");
|
||||
assert(adminMetrics.maxComponents <= 4, "municipal topology repair prevents excessive disconnected fragments");
|
||||
|
|
@ -544,7 +647,7 @@ try {
|
|||
const adminNameDuplicateRatio = map.adminCenters.length ? 1 - new Set(map.adminCenters.map((item) => item.name)).size / map.adminCenters.length : 0;
|
||||
assert(adminNameDuplicateRatio < 0.22, "duplicate municipal names stay low");
|
||||
assert(map.adminDebug.naturalCompartmentCount > adminMetrics.municipalityCount, "natural compartments are finer than municipalities");
|
||||
assert(map.adminDebug.targetMunicipalityCount >= 18 && map.adminDebug.actualMunicipalityCount >= 16, "municipality target and actual counts are dense enough");
|
||||
assert(map.adminDebug.targetMunicipalityCount >= 20 && map.adminDebug.targetMunicipalityCount <= 50 && map.adminDebug.actualMunicipalityCount >= 18, "municipality target and actual counts are dense enough");
|
||||
assert(map.adminDebug.changedAfterCompartmentAssignment > 0, "municipal compartment assignment is active");
|
||||
assert(Array.isArray(map.adminDebug.compartmentBorders) && map.adminDebug.compartmentBorders.length > map.adminBorders.length, "natural compartment borders are available for debug rendering");
|
||||
assert(map.entitiesForNames.every((item) => typeof item.name === "string"), "nameable entity names are strings");
|
||||
|
|
@ -553,6 +656,9 @@ try {
|
|||
assert(map.entitiesForNames.every((item) => String(item.name).length > 0), "empty generated names are prevented");
|
||||
assert(duplicateNameRatio < 0.18, "generated place-name duplicates stay low");
|
||||
assert(map.nameDebug && Array.isArray(map.nameDebug.emptyPools), "nameDebug reports empty pools");
|
||||
assert(map.nameDebug.oneKanjiAppendFallbackUsed === 0, "one-kanji append fallback is never used");
|
||||
assert((map.nameDebug.derivedNameCount || 0) <= Math.max(6, Math.ceil(map.adminCenters.length * 0.18)), "derived names do not dominate municipality names");
|
||||
assert((map.nameDebug.maxDerivedPerBase || 0) <= 2, "derived names per base stay small");
|
||||
assert(map.nameDebug.emptyPools.length === Object.values(NAME_KANJI_POOLS).filter((pool) => pool.length === 0).length, "nameDebug empty pools match configured pools");
|
||||
assert(map.nameDebug.selectedTemplateCounts && typeof map.nameDebug.selectedTemplateCounts === "object", "nameDebug selectedTemplateCounts exists");
|
||||
assert(map.nameDebug.selectedContextCounts && typeof map.nameDebug.selectedContextCounts === "object", "nameDebug selectedContextCounts exists");
|
||||
|
|
@ -581,6 +687,8 @@ try {
|
|||
assert(JSON.stringify([...againA.prefectureRegionId]) === JSON.stringify([...againB.prefectureRegionId]), "regional prefecture ids are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.adminDebug) === JSON.stringify(againB.adminDebug), "admin debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.regionalDebug) === JSON.stringify(againB.regionalDebug), "regional debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.transportDebug) === JSON.stringify(againB.transportDebug), "transport debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(transportConnectivityMetrics(againA)) === JSON.stringify(transportConnectivityMetrics(againB)), "transport connectivity metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.elevation]) === JSON.stringify([...againB.elevation]), "elevation is deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.ridgeField]) === JSON.stringify([...againB.ridgeField]), "ridge field is deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.river]) === JSON.stringify([...againB.river]), "river field is deterministic for the same seed");
|
||||
|
|
@ -599,12 +707,21 @@ try {
|
|||
assert(metrics.ridgeVariance > 0.003 && metrics.ridgeSinuosity > 0.010, `seed ${seedValue}: ridges have varied jagged structure`);
|
||||
assert(metrics.depositionSum > 0.1 && metrics.depositionTargetMean >= metrics.depositionOtherMean * 0.75, `seed ${seedValue}: deposition is active in plausible lowlands`);
|
||||
assert(metrics.riverValleyMean > metrics.nonRiverValleyMean, `seed ${seedValue}: rivers follow valley fields`);
|
||||
assert(seeded.terrainDebug?.primarySpineStrength > 0.08, `seed ${seedValue}: primary spine is strong enough`);
|
||||
assert((seeded.terrainDebug?.largeInlandLakeCount || 0) <= 1, `seed ${seedValue}: large inland lakes are rare`);
|
||||
assert((seeded.terrainDebug?.smallIslandCount || 0) <= 24, `seed ${seedValue}: small island speckles are limited`);
|
||||
assert(seeded.villages.length > 0 && seeded.markets.length > 0 && seeded.modernCities.length > 0, `seed ${seedValue}: settlements are generated`);
|
||||
assert(seeded.premodernRoads.length > 0 && seeded.railways.length > 0, `seed ${seedValue}: roads and railways are generated`);
|
||||
const seededTransport = transportConnectivityMetrics(seeded);
|
||||
assert(seeded.transportDebug?.requiredNodeCount === seededTransport.requiredCount, `seed ${seedValue}: required transport node count is exposed`);
|
||||
assert(seededTransport.reachableCount === seededTransport.requiredCount && seededTransport.largestRequiredComponent === seededTransport.requiredCount, `seed ${seedValue}: required transport nodes are connected`);
|
||||
assert(seededTransport.isolatedExternalGateways === 0 && seededTransport.isolatedMajorCities === 0, `seed ${seedValue}: no gateway or major city is isolated`);
|
||||
assert(seeded.adminId.length === size && seeded.adminBorders.length > 0 && seeded.regionalPrefectureBorders.length > 0, `seed ${seedValue}: admin and regional borders exist`);
|
||||
assert(seeded.adminCenters.every((item) => item.name && Array.from(String(item.name)).length >= 2), `seed ${seedValue}: every admin center has a valid name`);
|
||||
assert(seeded.entitiesForNames.some((item) => item.kind === "Municipal Center"), `seed ${seedValue}: admin labels are included in label candidates`);
|
||||
assert(seeded.adminCenters.every((item) => !(item.representativeFeatureName && String(item.name).startsWith(item.representativeFeatureName) && Array.from(String(item.name)).length === Array.from(String(item.representativeFeatureName)).length + 1)), `seed ${seedValue}: no dangling one-kanji admin suffix fallback`);
|
||||
assert(seeded.nameDebug?.oneKanjiAppendFallbackUsed === 0, `seed ${seedValue}: one-kanji append fallback stays unused`);
|
||||
assert((seeded.nameDebug?.derivedNameCount || 0) <= Math.max(6, Math.ceil(seeded.adminCenters.length * 0.18)), `seed ${seedValue}: derived names are bounded`);
|
||||
}
|
||||
const byDeposition = capitalNameMaps
|
||||
.map((seeded) => ({ deposition: seeded.terrainTemplate.deposition, lowlandRatio: terrainCoreMetrics(seeded).lowlandRatio }))
|
||||
|
|
@ -650,7 +767,7 @@ try {
|
|||
assert(seededRegional.maxComponents <= 5, `seed ${seed}: regional regions remain connected enough`);
|
||||
assert(seeded.adminDebug && seeded.adminDebug.compartmentCount > 0, `seed ${seed}: natural compartments are built`);
|
||||
assert(seeded.adminDebug.naturalCompartmentCount > metrics.municipalityCount, `seed ${seed}: compartments are finer than municipalities`);
|
||||
assert(seeded.adminDebug.targetMunicipalityCount >= 18 && seeded.adminDebug.actualMunicipalityCount >= 16, `seed ${seed}: municipality count is dense enough`);
|
||||
assert(seeded.adminDebug.targetMunicipalityCount >= 20 && seeded.adminDebug.actualMunicipalityCount >= 18, `seed ${seed}: municipality count is dense enough`);
|
||||
assert(seeded.adminDebug.changedAfterCompartmentAssignment > 0, `seed ${seed}: compartment assignment is not a no-op`);
|
||||
assert(Array.isArray(seeded.adminDebug.compartmentBorders) && seeded.adminDebug.compartmentBorders.length > seeded.adminBorders.length, `seed ${seed}: compartment border debug exists`);
|
||||
assert(seeded.regionalDebug?.changedAfterCompartmentAssignment > 0, `seed ${seed}: regional compartment assignment changes cells`);
|
||||
|
|
@ -658,7 +775,7 @@ try {
|
|||
assert(seeded.adminDebug.changedAfterCompartmentAssignment > 0 || seeded.adminDebug.changedAfterLandscapePartition > 0 || seeded.adminDebug.changedAfterSnap > 0, `seed ${seed}: municipal compartment or terrain passes change cells`);
|
||||
assert(seededSatellites.largeTooSmall.length === 0, `seed ${seed}: large satellites are not tiny independent municipalities`);
|
||||
assert(seededSatellites.independent.length < 3 || seededSatellites.small.length / seededSatellites.independent.length <= 0.35, `seed ${seed}: tiny satellite municipalities remain uncommon`);
|
||||
assert(metrics.municipalityCount >= 8, `seed ${seed}: municipality count remains reasonable`);
|
||||
assert(metrics.municipalityCount >= 18, `seed ${seed}: municipality count remains reasonable`);
|
||||
assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`);
|
||||
assert(metrics.maxComponents <= 5, `seed ${seed}: topology repair limits disconnected fragments`);
|
||||
assert(metrics.avgTarget > 0.14, `seed ${seed}: borders retain terrain-boundary affinity`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue