253 lines
8.4 KiB
JavaScript
253 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"],
|
|
["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();
|