first commit
This commit is contained in:
commit
32398c13e8
9 changed files with 4125 additions and 0 deletions
249
app.js
Normal file
249
app.js
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
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.ringExpressways || []).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 = [
|
||||
`<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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue