map/app.js
2026-05-21 22:03:14 +09:00

254 lines
8.4 KiB
JavaScript

import { generateMap } from "./mapGenerator.js";
import { drawMap } from "./renderer.js";
const modes = [
["all", "All"],
["terrain", "Terrain"],
["suitability", "Suitability"],
["history", "Premodern"],
["modern", "Modern"],
["roads", "Roads"],
["development", "Development"],
["landuse", "Land Use"],
["admin", "Municipal Borders"],
["terrain-debug", "Terrain Debug"],
["admin-debug", "Admin Debug"],
["borders-debug", "Borders Debug"],
];
const state = {
seedText: "114514",
mode: "all",
showFeatures: true,
showLabels: true,
map: null,
};
const canvas = document.getElementById("mapCanvas");
const seedInput = document.getElementById("seed");
const randomSeedButton = document.getElementById("randomSeed");
const showFeaturesInput = document.getElementById("showFeatures");
const showLabelsInput = document.getElementById("showLabels");
const modeGrid = document.getElementById("modeGrid");
const statsEl = document.getElementById("stats");
const idsEl = document.getElementById("nameIds");
const tooltipEl = document.getElementById("mapTooltip");
function parseSeed(seedText) {
const numeric = Number.parseInt(seedText, 10);
if (Number.isFinite(numeric)) return numeric >>> 0;
let hash = 2166136261;
for (const ch of seedText) hash = Math.imul(hash ^ ch.charCodeAt(0), 16777619);
return hash >>> 0;
}
function insideCount(items) {
return items.filter((item) => item.insidePrefecture).length;
}
function outsideCount(items) {
return items.length - insideCount(items);
}
function countText(items) {
return `${insideCount(items)} / outside ${outsideCount(items)}`;
}
function getStats(map) {
return [
["Villages", countText(map.villages)],
["Market Towns", countText(map.markets)],
["Castles", countText(map.castles)],
["Premodern Roads", map.premodernRoads.length],
["Minor Roads", map.minorRoads.length],
["Prefectural Capital", map.prefecturalCapital?.name || "-"],
["Modern Cities", countText(map.modernCities)],
["Ports", `${map.ports.filter((p) => p.portClass === "major").length} major / ${map.ports.filter((p) => p.portClass === "regional").length} regional / ${map.ports.filter((p) => p.portClass === "fishing").length} fishing / ${map.ports.filter((p) => p.portClass === "lake").length} lake`],
["Satellite Cities", countText(map.satelliteCities || [])],
["Population", (map.totalPopulation || 0).toLocaleString()],
["Rivers", `${map.mainRivers.length} main / ${(map.tributaryRivers || []).length} tributary / ${(map.smallStreams || []).length} hidden streams`],
["Neighbor Prefecture Borders", (map.regionalPrefectureBorders || []).length],
["Harbor Works", (map.harborWorks || []).length],
["Rail Lines", map.railways.length + map.branchRailways.length + (map.ringRailways || []).length + map.externalRailways.length],
["Industrial Zones", countText(map.industrialZones)],
["National Roads", map.nationalRoads.length + (map.ringRoads || []).length],
["Expressways", map.expressways.length + map.externalExpressways.length],
["External Gateways", map.externalGateways.length],
["Interchanges", countText(map.interchanges)],
["Logistics Parks", countText(map.logisticsParks)],
["New Towns", countText(map.newTowns)],
["Municipalities", map.adminCenters.length],
["Admin changed cells", map.adminDebug ? `${map.adminDebug.changedAfterLandscapePartition || 0} partition / ${map.adminDebug.changedAfterSnap || 0} snap` : "-"],
["Regional changed cells", map.regionalDebug?.regionalChangedAfterNaturalPartition ?? "-"],
];
}
function renderStats(map) {
statsEl.innerHTML = "";
for (const [labelText, valueText] of getStats(map)) {
const row = document.createElement("div");
row.className = "stat-row";
const label = document.createElement("span");
label.textContent = labelText;
const value = document.createElement("strong");
value.textContent = String(valueText);
row.append(label, value);
statsEl.append(row);
}
}
function renderNameIds(map) {
idsEl.innerHTML = "";
for (const entity of map.entitiesForNames.slice(0, 120)) {
const row = document.createElement("div");
row.className = "id-row";
const code = document.createElement("code");
code.textContent = entity.id;
const name = document.createElement("span");
const population = entity.population ? ` / ${entity.population.toLocaleString()} people` : "";
name.textContent = `${entity.name} / ${entity.kind}${population}`;
row.append(code, name);
idsEl.append(row);
}
}
function nearestEntity(map, x, y, maxDistance = 5) {
const groups = [
...(map.modernCities || []),
...(map.ports || []),
...(map.stations || []),
...(map.interchanges || []),
...(map.industrialZones || []),
...(map.logisticsParks || []),
...(map.newTowns || []),
...(map.castles || []),
...(map.markets || []),
...(map.villages || []),
...(map.adminCenters || []),
];
let best = null;
let bestD = maxDistance;
for (const item of groups) {
const d = Math.hypot(item.x - x, item.y - y);
if (d < bestD) { best = item; bestD = d; }
}
return best;
}
function landuseName(value) {
return {
0: "Agriculture",
1: "Plain",
2: "Old urban area",
3: "CBD / DID core",
4: "Suburban urban area",
5: "Industrial zone",
6: "Logistics area",
7: "New town",
8: "Roadside development",
9: "Forest / rural land",
}[value] || "Land";
}
function adminName(map, adminId) {
const center = (map.adminCenters || [])[adminId];
return center?.name || (adminId >= 0 ? `Municipality ${adminId + 1}` : "-");
}
function updateTooltip(event) {
if (!state.map || !tooltipEl) return;
const rect = canvas.getBoundingClientRect();
const x = Math.floor((event.clientX - rect.left) / rect.width * state.map.width);
const y = Math.floor((event.clientY - rect.top) / rect.height * state.map.height);
if (x < 0 || y < 0 || x >= state.map.width || y >= state.map.height) {
tooltipEl.classList.remove("visible");
return;
}
const i = y * state.map.width + x;
const entity = nearestEntity(state.map, x, y);
const elevation = state.map.elevation?.[i] ?? 0;
const density = state.map.populationDensity?.[i] ?? 0;
const lines = [
`<strong>${entity ? `${entity.name} / ${entity.kind}` : `${x}, ${y}`}</strong>`,
`Admin: ${adminName(state.map, state.map.adminId?.[i] ?? -1)}`,
`Land: ${state.map.sea?.[i] ? "Sea" : landuseName(state.map.landuse?.[i])}`,
`Elevation: ${elevation.toFixed(3)} / Slope: ${(state.map.slope?.[i] ?? 0).toFixed(3)}`,
`River: ${(state.map.river?.[i] ?? 0).toFixed(2)} / Density: ${density.toFixed(2)}`,
];
if (entity?.population) lines.splice(1, 0, `Population: ${entity.population.toLocaleString()}`);
tooltipEl.innerHTML = lines.join("<br>");
tooltipEl.style.left = `${event.clientX - rect.left + 14}px`;
tooltipEl.style.top = `${event.clientY - rect.top + 14}px`;
tooltipEl.classList.add("visible");
}
function renderModeButtons() { modeGrid.innerHTML = "";
for (const [key, label] of modes) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.className = key === state.mode ? "mode-button active" : "mode-button";
button.addEventListener("click", () => {
state.mode = key;
renderModeButtons();
redraw();
});
modeGrid.append(button);
}
}
function regenerate() {
state.seedText = seedInput.value;
state.map = generateMap(parseSeed(state.seedText));
renderStats(state.map);
renderNameIds(state.map);
redraw();
}
function redraw() {
if (!state.map) return;
drawMap(canvas, state.map, {
mode: state.mode,
showFeatures: state.showFeatures,
showLabels: state.showLabels,
});
}
function init() {
renderModeButtons();
seedInput.addEventListener("change", regenerate);
seedInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") regenerate();
});
randomSeedButton.addEventListener("click", () => {
seedInput.value = String(Math.floor(Math.random() * 9999999));
regenerate();
});
showFeaturesInput.addEventListener("change", () => {
state.showFeatures = showFeaturesInput.checked;
redraw();
});
showLabelsInput.addEventListener("change", () => {
state.showLabels = showLabelsInput.checked;
redraw();
});
canvas.addEventListener("mousemove", updateTooltip);
canvas.addEventListener("mouseleave", () => tooltipEl?.classList.remove("visible"));
regenerate();
}
init();