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"], ]; 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], ]; } 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 = [ `${entity ? `${entity.name} / ${entity.kind}` : `${x}, ${y}`}`, `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("
"); 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();