import { generateMapAsync } from "./mapGenerator.js"; import { drawMap } from "./renderer.js"; import { landuseLabel } from "./landuseCodes.js"; const modes = [ ["all", "All"], ["terrain", "Terrain"], ["history", "Premodern"], ["modern", "Modern"], ["development", "Development"], ["landuse", "Land Use"], ["admin", "Municipal Borders"], ["admin-debug", "Admin Debug"], ["borders-debug", "Borders Debug"], ]; const state = { seedText: "114514", mode: "all", showFeatures: true, showLabels: true, map: null, hoverEntities: [], }; 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 tooltipEl = document.getElementById("mapTooltip"); const progressEl = document.getElementById("generationProgress"); const progressStageEl = document.getElementById("generationProgressStage"); const progressTimingsEl = document.getElementById("generationProgressTimings"); let generationStartedAt = 0; let generationCurrentStage = ""; let generationTimer = null; 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 formatMs(ms) { if (!Number.isFinite(ms)) return "-"; return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${Math.round(ms)}ms`; } function renderTimingRows(timings = []) { if (!progressTimingsEl) return; progressTimingsEl.innerHTML = ""; for (const row of timings) { const item = document.createElement("div"); item.className = "progress-timing-row"; const label = document.createElement("span"); label.textContent = row.label; const value = document.createElement("strong"); value.textContent = formatMs(row.ms); item.append(label, value); progressTimingsEl.append(item); } } function updateGenerationProgress(event) { if (!progressEl) return; progressEl.classList.remove("hidden"); if (event?.status === "start") generationCurrentStage = event.label || "Preparing"; if (progressStageEl) { const elapsed = generationStartedAt ? ` / elapsed ${formatMs(performance.now() - generationStartedAt)}` : ""; progressStageEl.textContent = event?.status === "done" ? `Completed: ${event.label} / ${formatMs(event.ms)}${elapsed}` : `Running: ${event?.label || generationCurrentStage || "Preparing"}${elapsed}`; } renderTimingRows(event?.timings || []); } function setProgressVisible(visible, message = "Preparing") { if (!progressEl) return; progressEl.classList.toggle("hidden", !visible); if (visible) { generationStartedAt = performance.now(); generationCurrentStage = message; if (generationTimer) window.clearInterval(generationTimer); generationTimer = window.setInterval(() => { if (progressStageEl && !progressEl.classList.contains("hidden")) { progressStageEl.textContent = `Running: ${generationCurrentStage || "Preparing"} / elapsed ${formatMs(performance.now() - generationStartedAt)}`; } }, 100); } else if (generationTimer) { window.clearInterval(generationTimer); generationTimer = null; } if (progressStageEl) progressStageEl.textContent = message; if (visible) renderTimingRows([]); } function nextFrame() { return new Promise((resolve) => requestAnimationFrame(() => resolve())); } function getStats(map) { return [ ["Map Type", map.terrainTemplate?.terrainTypeLabel || map.terrainDebug?.terrainTypeLabel || "-"], ["Terrain ID", map.terrainTemplate?.terrainType || map.terrainDebug?.terrainType || "-"], ["Generation Total", map.generationTotalMs ? formatMs(map.generationTotalMs) : "-"], ...(map.generationTimings || []).map((row) => [`Time: ${row.label}`, formatMs(row.ms)]), ["Villages", countText(map.villages)], ["Market Towns", countText(map.markets)], ["Castles", countText(map.castles)], ["Premodern Roads", map.premodernRoads.length], ["Minor Roads", map.minorRoads.length], ["Prefecture", map.prefectureName || "-"], ["Neighbor Prefectures", (map.neighborPrefectures || []).map((p) => p.name).join(" / ") || "-"], ["Neighbor Features", map.neighborPrefectureDetails ? `${map.neighborPrefectureDetails.cities?.length || 0} cities / ${map.neighborPrefectureDetails.adminCenters?.length || 0} municipalities / ${map.neighborPrefectureDetails.roads?.length || 0} roads` : "-"], ["Prefectural Capital", map.prefecturalCapital?.name || "-"], ["Regional Capitals", (map.modernCities || []).filter((p) => p.isRegionalCapital).length], ["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], ["Rail Lines", map.railways.length + map.branchRailways.length + (map.ringRailways || []).length + map.externalRailways.length], ["Industrial Zones", countText(map.industrialZones)], ["National Roads", `${map.nationalRoads.length} / pop cover ${Math.round((map.transportDebug?.nationalRoadPopulationCoverage || 0) * 100)}% / uncovered ${(map.transportDebug?.nationalRoadUncoveredPopulation || 0).toLocaleString()}`], ["General Ring Roads", (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` : "-"], ["Prefecture source", map.regionalDebug?.prefectureSource ?? "-"], ]; } 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 buildHoverEntities(map) { return [ ...(map.modernCities || []), ...(map.ports || []), ...(map.stations || []), ...(map.interchanges || []), ...(map.industrialZones || []), ...(map.logisticsParks || []), ...(map.newTowns || []), ...(map.castles || []), ...(map.markets || []), ...(map.villages || []), ...(map.adminCenters || []), ]; } function nearestEntity(items, x, y, maxDistance = 5) { let best = null; let bestD = maxDistance; for (const item of items) { const d = Math.hypot(item.x - x, item.y - y); if (d < bestD) { best = item; bestD = d; } } return best; } function landuseName(value) { return landuseLabel(value); } function adminName(map, adminId) { const center = (map.adminCenters || [])[adminId]; return center?.name || (adminId >= 0 ? `Municipality ${adminId + 1}` : "-"); } function adminPopulation(map, adminId) { const center = (map.adminCenters || [])[adminId]; return Number.isFinite(center?.municipalityPopulation) ? center.municipalityPopulation : null; } function prefectureNameForCell(map, i) { const id = map.prefectureRegionId?.[i] ?? -1; const region = (map.prefectureRegions || []).find((p) => p.id === id); return region?.name || (id >= 0 ? `Prefecture ${id + 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.hoverEntities, x, y); const elevation = state.map.elevation?.[i] ?? 0; const density = state.map.populationDensity?.[i] ?? 0; const hoveredAdminId = state.map.adminId?.[i] ?? -1; const hoveredAdminPopulation = adminPopulation(state.map, hoveredAdminId); const lines = [ `${entity ? `${entity.name} / ${entity.kind}` : `${x}, ${y}`}`, `Prefecture: ${prefectureNameForCell(state.map, i)}`, `Admin: ${adminName(state.map, hoveredAdminId)}`, `Admin Pop: ${hoveredAdminPopulation === null ? "-" : hoveredAdminPopulation.toLocaleString()}`, `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("
"); const margin = 8; const offset = 14; const maxLeft = Math.max(margin, rect.width - tooltipEl.offsetWidth - margin); const maxTop = Math.max(margin, rect.height - tooltipEl.offsetHeight - margin); const desiredLeft = event.clientX - rect.left + offset; const desiredTop = event.clientY - rect.top + offset; tooltipEl.style.left = `${Math.min(Math.max(margin, desiredLeft), maxLeft)}px`; tooltipEl.style.top = `${Math.min(Math.max(margin, desiredTop), maxTop)}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); } } async function regenerate() { state.seedText = seedInput.value; setProgressVisible(true, "Preparing generation..."); await nextFrame(); try { state.map = await generateMapAsync(parseSeed(state.seedText), { onProgress: updateGenerationProgress }); state.hoverEntities = buildHoverEntities(state.map); renderStats(state.map); redraw(); if (progressStageEl) progressStageEl.textContent = `Done in ${formatMs(state.map.generationTotalMs || 0)}`; renderTimingRows(state.map.generationTimings || []); window.setTimeout(() => setProgressVisible(false), 900); } catch (error) { if (progressStageEl) progressStageEl.textContent = `Generation failed: ${error?.message || error}`; throw error; } } 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();