infinity expantion
This commit is contained in:
parent
c523bf4380
commit
ea0067dc0a
10 changed files with 2792 additions and 70 deletions
380
app.js
380
app.js
|
|
@ -1,6 +1,10 @@
|
|||
import { generateMapAsync } from "./mapGenerator.js";
|
||||
import { drawMap } from "./renderer.js";
|
||||
import { landuseLabel } from "./landuseCodes.js";
|
||||
import { CELL_SIZE, MAP_H, MAP_W } from "./mapUtils.js";
|
||||
import { clampCameraToWorld, createInitialCamera, createWorldMap } from "./worldMap.js";
|
||||
import { getViewportMap } from "./worldViewport.js";
|
||||
import { PATCH_MIN_AREA, PATCH_MIN_HEIGHT, PATCH_MIN_WIDTH, buildPatchRects, generatePatch, validatePatchRect } from "./mapPatch.js";
|
||||
|
||||
const modes = [
|
||||
["all", "All"],
|
||||
|
|
@ -21,19 +25,28 @@ const state = {
|
|||
showFeatures: true,
|
||||
showLabels: true,
|
||||
map: null,
|
||||
world: null,
|
||||
camera: { x: 0, y: 0 },
|
||||
viewportMap: null,
|
||||
hoverEntities: [],
|
||||
selectionRect: null,
|
||||
lastPatchResult: null,
|
||||
};
|
||||
|
||||
const canvas = document.getElementById("mapCanvas");
|
||||
const canvasShell = document.querySelector(".canvas-shell");
|
||||
const seedInput = document.getElementById("seed");
|
||||
const generationTypeInput = document.getElementById("generationType");
|
||||
const patchTerrainTypeInput = document.getElementById("patchTerrainType");
|
||||
const generatePatchButton = document.getElementById("generatePatch");
|
||||
const patchStatusEl = document.getElementById("patchStatus");
|
||||
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 selectionEl = document.getElementById("mapSelection");
|
||||
const progressEl = document.getElementById("generationProgress");
|
||||
const progressStageEl = document.getElementById("generationProgressStage");
|
||||
const progressTimingsEl = document.getElementById("generationProgressTimings");
|
||||
|
|
@ -41,66 +54,253 @@ let generationStartedAt = 0;
|
|||
let generationCurrentStage = "";
|
||||
let generationTimer = null;
|
||||
|
||||
const panState = { keys: new Set(), raf: null, lastTime: 0, speedPxPerSecond: 520 };
|
||||
const dragState = {
|
||||
mode: null,
|
||||
pointerId: null,
|
||||
startClientX: 0,
|
||||
startClientY: 0,
|
||||
startCameraX: 0,
|
||||
startCameraY: 0,
|
||||
selectStart: null,
|
||||
selectEnd: null,
|
||||
pendingCamera: null,
|
||||
panRaf: null,
|
||||
};
|
||||
|
||||
function activeMap() {
|
||||
return state.viewportMap || state.map;
|
||||
}
|
||||
|
||||
function mapClientToCell(event) {
|
||||
if (!state.map) return null;
|
||||
const map = activeMap();
|
||||
if (!map) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) return null;
|
||||
const relX = (event.clientX - rect.left) / rect.width;
|
||||
const relY = (event.clientY - rect.top) / rect.height;
|
||||
return {
|
||||
x: Math.floor(relX * state.map.width),
|
||||
y: Math.floor(relY * state.map.height),
|
||||
x: Math.floor(relX * map.width),
|
||||
y: Math.floor(relY * map.height),
|
||||
};
|
||||
}
|
||||
|
||||
function isEditableTarget(target) {
|
||||
if (!target) return false;
|
||||
const tag = target.tagName?.toLowerCase?.();
|
||||
return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable;
|
||||
function viewportCellToWorldCell(cell) {
|
||||
if (!cell || !state.camera) return null;
|
||||
return {
|
||||
x: Math.round(state.camera.x || 0) + cell.x,
|
||||
y: Math.round(state.camera.y || 0) + cell.y,
|
||||
};
|
||||
}
|
||||
|
||||
function panFrame(time) {
|
||||
if (!canvasShell || panState.keys.size === 0) {
|
||||
panState.raf = null;
|
||||
panState.lastTime = 0;
|
||||
function clampCanvasPoint(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.min(Math.max(event.clientX - rect.left, 0), rect.width),
|
||||
y: Math.min(Math.max(event.clientY - rect.top, 0), rect.height),
|
||||
};
|
||||
}
|
||||
|
||||
function updateSelectionOverlay() {
|
||||
if (!selectionEl || !dragState.selectStart || !dragState.selectEnd) return;
|
||||
const x0 = Math.min(dragState.selectStart.x, dragState.selectEnd.x);
|
||||
const y0 = Math.min(dragState.selectStart.y, dragState.selectEnd.y);
|
||||
const x1 = Math.max(dragState.selectStart.x, dragState.selectEnd.x);
|
||||
const y1 = Math.max(dragState.selectStart.y, dragState.selectEnd.y);
|
||||
selectionEl.style.display = "block";
|
||||
selectionEl.style.left = `${canvas.offsetLeft + x0}px`;
|
||||
selectionEl.style.top = `${canvas.offsetTop + y0}px`;
|
||||
selectionEl.style.width = `${Math.max(1, x1 - x0)}px`;
|
||||
selectionEl.style.height = `${Math.max(1, y1 - y0)}px`;
|
||||
const liveRect = selectionPixelsToCells(dragState.selectStart, dragState.selectEnd);
|
||||
const validation = validatePatchRect(liveRect, state.world);
|
||||
selectionEl.classList.toggle("invalid", !validation.ok);
|
||||
if (generatePatchButton) generatePatchButton.disabled = true;
|
||||
if (patchStatusEl) {
|
||||
const current = validation.rect || liveRect;
|
||||
patchStatusEl.textContent = validation.ok
|
||||
? `Selected: ${formatRectSize(validation.rect)}. Release to confirm patch selection.`
|
||||
: `${validation.reason} Current: ${formatRectSize(current)}.`;
|
||||
patchStatusEl.classList.toggle("invalid", !validation.ok);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectionOverlayFromWorldRect() {
|
||||
if (!selectionEl || !state.selectionRect || !state.camera || !activeMap()) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) return;
|
||||
const map = activeMap();
|
||||
const cameraX = Math.round(state.camera.x || 0);
|
||||
const cameraY = Math.round(state.camera.y || 0);
|
||||
const vx0 = (state.selectionRect.x0 - cameraX) / map.width * rect.width;
|
||||
const vy0 = (state.selectionRect.y0 - cameraY) / map.height * rect.height;
|
||||
const vx1 = (state.selectionRect.x1 - cameraX) / map.width * rect.width;
|
||||
const vy1 = (state.selectionRect.y1 - cameraY) / map.height * rect.height;
|
||||
const x0 = Math.min(Math.max(Math.min(vx0, vx1), 0), rect.width);
|
||||
const y0 = Math.min(Math.max(Math.min(vy0, vy1), 0), rect.height);
|
||||
const x1 = Math.min(Math.max(Math.max(vx0, vx1), 0), rect.width);
|
||||
const y1 = Math.min(Math.max(Math.max(vy0, vy1), 0), rect.height);
|
||||
if (x1 - x0 < 1 || y1 - y0 < 1) {
|
||||
selectionEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const dt = panState.lastTime ? Math.min(0.05, (time - panState.lastTime) / 1000) : 0;
|
||||
panState.lastTime = time;
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
if (panState.keys.has("a")) dx -= 1;
|
||||
if (panState.keys.has("d")) dx += 1;
|
||||
if (panState.keys.has("w")) dy -= 1;
|
||||
if (panState.keys.has("s")) dy += 1;
|
||||
if (dx || dy) {
|
||||
const normalizer = dx && dy ? Math.SQRT1_2 : 1;
|
||||
const amount = panState.speedPxPerSecond * dt;
|
||||
canvasShell.scrollLeft += dx * normalizer * amount;
|
||||
canvasShell.scrollTop += dy * normalizer * amount;
|
||||
tooltipEl?.classList.remove("visible");
|
||||
selectionEl.style.display = "block";
|
||||
selectionEl.style.left = `${canvas.offsetLeft + x0}px`;
|
||||
selectionEl.style.top = `${canvas.offsetTop + y0}px`;
|
||||
selectionEl.style.width = `${Math.max(1, x1 - x0)}px`;
|
||||
selectionEl.style.height = `${Math.max(1, y1 - y0)}px`;
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
selectionEl.classList.toggle("invalid", !validation.ok);
|
||||
}
|
||||
|
||||
function formatRectSize(rect) {
|
||||
if (!rect) return "-";
|
||||
const w = Math.max(0, rect.x1 - rect.x0);
|
||||
const h = Math.max(0, rect.y1 - rect.y0);
|
||||
return `${w} x ${h} cells / ${(w * h).toLocaleString()} cells`;
|
||||
}
|
||||
|
||||
function updatePatchControls() {
|
||||
if (!patchStatusEl && !generatePatchButton) return;
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
if (generatePatchButton) generatePatchButton.disabled = !validation.ok;
|
||||
if (!patchStatusEl) return;
|
||||
if (!state.selectionRect) {
|
||||
patchStatusEl.textContent = `Right-drag an area. Minimum: ${PATCH_MIN_WIDTH} x ${PATCH_MIN_HEIGHT} cells and ${PATCH_MIN_AREA.toLocaleString()} cells.`;
|
||||
patchStatusEl.classList.toggle("invalid", false);
|
||||
return;
|
||||
}
|
||||
panState.raf = requestAnimationFrame(panFrame);
|
||||
if (!validation.ok) {
|
||||
patchStatusEl.textContent = `${validation.reason} Current: ${formatRectSize(validation.rect || state.selectionRect)}.`;
|
||||
patchStatusEl.classList.toggle("invalid", true);
|
||||
return;
|
||||
}
|
||||
const rects = buildPatchRects(validation.rect, state.world);
|
||||
const patchText = state.lastPatchResult
|
||||
? ` Last patch: ${state.lastPatchResult.label}, terrain ${state.lastPatchResult.updatedCells.toLocaleString()} cells, coast ${state.lastPatchResult.coastCellsChanged || 0}, natural ${state.lastPatchResult.naturalRegionsUpdated || 0}${state.lastPatchResult.humanGeography?.ok ? `, connectors ${(state.lastPatchResult.humanGeography.roadConnectorsCreated || 0) + (state.lastPatchResult.humanGeography.railwayConnectorsCreated || 0)}, admin ${state.lastPatchResult.humanGeography.adminCellsReassigned || 0}, invalid ports ${state.lastPatchResult.humanGeography.invalidPortsRemoved || 0}` : ""}.`
|
||||
: "";
|
||||
patchStatusEl.textContent = `Core: ${formatRectSize(rects.coreRect)}. Write: ${formatRectSize(rects.writeRect)}. Repair: ${formatRectSize(rects.repairRect)}.${patchText}`;
|
||||
patchStatusEl.classList.toggle("invalid", false);
|
||||
}
|
||||
|
||||
function startKeyboardPan() {
|
||||
if (panState.raf == null) panState.raf = requestAnimationFrame(panFrame);
|
||||
function clearDragMode() {
|
||||
dragState.mode = null;
|
||||
dragState.pointerId = null;
|
||||
dragState.pendingCamera = null;
|
||||
if (dragState.panRaf != null) {
|
||||
cancelAnimationFrame(dragState.panRaf);
|
||||
dragState.panRaf = null;
|
||||
}
|
||||
canvasShell?.classList.remove("panning", "selecting");
|
||||
}
|
||||
|
||||
function handlePanKeyDown(event) {
|
||||
const key = event.key?.toLowerCase?.();
|
||||
if (!key || !"wasd".includes(key) || isEditableTarget(event.target)) return;
|
||||
panState.keys.add(key);
|
||||
startKeyboardPan();
|
||||
function schedulePanRedraw(camera) {
|
||||
dragState.pendingCamera = camera;
|
||||
if (dragState.panRaf != null) return;
|
||||
dragState.panRaf = requestAnimationFrame(() => {
|
||||
dragState.panRaf = null;
|
||||
if (!dragState.pendingCamera) return;
|
||||
const next = dragState.pendingCamera;
|
||||
dragState.pendingCamera = null;
|
||||
if (next.x === state.camera.x && next.y === state.camera.y) return;
|
||||
state.camera = next;
|
||||
redraw({ fastTerrain: true });
|
||||
});
|
||||
}
|
||||
|
||||
function hideSelectionOverlay() {
|
||||
dragState.selectStart = null;
|
||||
dragState.selectEnd = null;
|
||||
state.selectionRect = null;
|
||||
if (selectionEl) selectionEl.style.display = "none";
|
||||
updatePatchControls();
|
||||
}
|
||||
|
||||
function selectionPixelsToCells(start, end) {
|
||||
const map = activeMap();
|
||||
if (!map || !start || !end) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) return null;
|
||||
const localX0 = Math.floor(Math.min(start.x, end.x) / rect.width * map.width);
|
||||
const localY0 = Math.floor(Math.min(start.y, end.y) / rect.height * map.height);
|
||||
const localX1 = Math.ceil(Math.max(start.x, end.x) / rect.width * map.width);
|
||||
const localY1 = Math.ceil(Math.max(start.y, end.y) / rect.height * map.height);
|
||||
const cameraX = Math.round(state.camera?.x || 0);
|
||||
const cameraY = Math.round(state.camera?.y || 0);
|
||||
return {
|
||||
x0: cameraX + Math.min(Math.max(localX0, 0), map.width - 1),
|
||||
y0: cameraY + Math.min(Math.max(localY0, 0), map.height - 1),
|
||||
x1: cameraX + Math.min(Math.max(localX1, 1), map.width),
|
||||
y1: cameraY + Math.min(Math.max(localY1, 1), map.height),
|
||||
};
|
||||
}
|
||||
|
||||
function handleMapPointerDown(event) {
|
||||
if (!state.world || !canvasShell) return;
|
||||
if (event.button !== 0 && event.button !== 2) return;
|
||||
dragState.pointerId = event.pointerId;
|
||||
dragState.startClientX = event.clientX;
|
||||
dragState.startClientY = event.clientY;
|
||||
dragState.startCameraX = state.camera.x;
|
||||
dragState.startCameraY = state.camera.y;
|
||||
tooltipEl?.classList.remove("visible");
|
||||
|
||||
if (event.button === 0) {
|
||||
dragState.mode = "pan";
|
||||
canvasShell.classList.add("panning");
|
||||
} else {
|
||||
dragState.mode = "select";
|
||||
dragState.selectStart = clampCanvasPoint(event);
|
||||
dragState.selectEnd = dragState.selectStart;
|
||||
canvasShell.classList.add("selecting");
|
||||
updateSelectionOverlay();
|
||||
}
|
||||
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handlePanKeyUp(event) {
|
||||
const key = event.key?.toLowerCase?.();
|
||||
if (!key || !"wasd".includes(key)) return;
|
||||
panState.keys.delete(key);
|
||||
function handleMapPointerMove(event) {
|
||||
if (!dragState.mode || dragState.pointerId !== event.pointerId || !canvasShell) return;
|
||||
tooltipEl?.classList.remove("visible");
|
||||
|
||||
if (dragState.mode === "pan") {
|
||||
const dxCells = Math.round((event.clientX - dragState.startClientX) / CELL_SIZE);
|
||||
const dyCells = Math.round((event.clientY - dragState.startClientY) / CELL_SIZE);
|
||||
const nextCamera = clampCameraToWorld({
|
||||
x: dragState.startCameraX - dxCells,
|
||||
y: dragState.startCameraY - dyCells,
|
||||
}, state.world, MAP_W, MAP_H);
|
||||
schedulePanRedraw(nextCamera);
|
||||
} else if (dragState.mode === "select") {
|
||||
dragState.selectEnd = clampCanvasPoint(event);
|
||||
updateSelectionOverlay();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleMapPointerUp(event) {
|
||||
if (dragState.pointerId !== event.pointerId) return;
|
||||
const wasPanning = dragState.mode === "pan";
|
||||
if (dragState.mode === "select") {
|
||||
dragState.selectEnd = clampCanvasPoint(event);
|
||||
const width = Math.abs(dragState.selectEnd.x - dragState.selectStart.x);
|
||||
const height = Math.abs(dragState.selectEnd.y - dragState.selectStart.y);
|
||||
if (width >= 4 && height >= 4) {
|
||||
state.selectionRect = selectionPixelsToCells(dragState.selectStart, dragState.selectEnd);
|
||||
updateSelectionOverlayFromWorldRect();
|
||||
updatePatchControls();
|
||||
} else {
|
||||
hideSelectionOverlay();
|
||||
}
|
||||
}
|
||||
canvas.releasePointerCapture?.(event.pointerId);
|
||||
if (wasPanning && dragState.pendingCamera) {
|
||||
state.camera = dragState.pendingCamera;
|
||||
dragState.pendingCamera = null;
|
||||
}
|
||||
clearDragMode();
|
||||
if (wasPanning) redraw({ fastTerrain: false });
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
|
@ -283,29 +483,35 @@ function prefectureNameForCell(map, i) {
|
|||
}
|
||||
|
||||
function updateTooltip(event) {
|
||||
if (!state.map || !tooltipEl) return;
|
||||
const map = activeMap();
|
||||
if (!map || !tooltipEl || dragState.mode) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const cell = mapClientToCell(event);
|
||||
if (!cell) return;
|
||||
const { x, y } = cell;
|
||||
if (x < 0 || y < 0 || x >= state.map.width || y >= state.map.height) {
|
||||
if (x < 0 || y < 0 || x >= map.width || y >= map.height) {
|
||||
tooltipEl.classList.remove("visible");
|
||||
return;
|
||||
}
|
||||
const i = y * state.map.width + x;
|
||||
const i = y * map.width + x;
|
||||
const worldCell = viewportCellToWorldCell({ x, y });
|
||||
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 elevation = map.elevation?.[i] ?? 0;
|
||||
const density = map.populationDensity?.[i] ?? 0;
|
||||
const hoveredAdminId = map.adminId?.[i] ?? -1;
|
||||
const hoveredAdminPopulation = adminPopulation(map, hoveredAdminId);
|
||||
const coordinateText = worldCell ? `World ${worldCell.x}, ${worldCell.y} / View ${x}, ${y}` : `${x}, ${y}`;
|
||||
const entityTitle = entity
|
||||
? `${entity.name || entity.facilityLabel || entity.kind || "Feature"} / ${entity.kind || "Feature"}`
|
||||
: coordinateText;
|
||||
const lines = [
|
||||
`<strong>${entity ? `${entity.name} / ${entity.kind}` : `${x}, ${y}`}</strong>`,
|
||||
`Prefecture: ${prefectureNameForCell(state.map, i)}`,
|
||||
`Admin: ${adminName(state.map, hoveredAdminId)}`,
|
||||
`<strong>${entityTitle}</strong>`,
|
||||
`Prefecture: ${prefectureNameForCell(map, i)}`,
|
||||
`Admin: ${adminName(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)}`,
|
||||
`Land: ${map.sea?.[i] ? "Sea" : landuseName(map.landuse?.[i])}`,
|
||||
`Elevation: ${elevation.toFixed(3)} / Slope: ${(map.slope?.[i] ?? 0).toFixed(3)}`,
|
||||
`River: ${(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>");
|
||||
|
|
@ -343,7 +549,10 @@ async function regenerate() {
|
|||
await nextFrame();
|
||||
try {
|
||||
state.map = await generateMapAsync(parseSeed(state.seedText), { onProgress: updateGenerationProgress, terrainType: state.generationType });
|
||||
state.hoverEntities = buildHoverEntities(state.map);
|
||||
state.world = createWorldMap(state.map);
|
||||
state.camera = createInitialCamera(state.world);
|
||||
state.lastPatchResult = null;
|
||||
hideSelectionOverlay();
|
||||
renderStats(state.map);
|
||||
redraw();
|
||||
if (progressStageEl) progressStageEl.textContent = `Done in ${formatMs(state.map.generationTotalMs || 0)}`;
|
||||
|
|
@ -355,13 +564,62 @@ async function regenerate() {
|
|||
}
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
if (!state.map) return;
|
||||
drawMap(canvas, state.map, {
|
||||
|
||||
function derivePatchSeed(rect, terrainType) {
|
||||
let h = parseSeed(state.seedText) ^ 0x9e3779b9;
|
||||
h = Math.imul(h ^ (rect.x0 | 0), 1664525) >>> 0;
|
||||
h = Math.imul(h ^ (rect.y0 | 0), 1013904223) >>> 0;
|
||||
h = Math.imul(h ^ (rect.x1 | 0), 2246822519) >>> 0;
|
||||
h = Math.imul(h ^ (rect.y1 | 0), 3266489917) >>> 0;
|
||||
for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
async function generateSelectedPatch() {
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
if (!validation.ok) {
|
||||
updatePatchControls();
|
||||
return;
|
||||
}
|
||||
const terrainType = patchTerrainTypeInput?.value || generationTypeInput?.value || "auto";
|
||||
const seed = derivePatchSeed(validation.rect, terrainType);
|
||||
setProgressVisible(true, "Generating selected patch...");
|
||||
await nextFrame();
|
||||
try {
|
||||
const result = generatePatch(state.world, validation.rect, { terrainType, seed });
|
||||
if (!result.ok) {
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch failed: ${result.reason || "invalid selection"}`;
|
||||
updatePatchControls();
|
||||
window.setTimeout(() => setProgressVisible(false), 1200);
|
||||
return;
|
||||
}
|
||||
state.lastPatchResult = result;
|
||||
redraw();
|
||||
renderStats(state.map);
|
||||
updatePatchControls();
|
||||
const human = result.humanGeography;
|
||||
const humanText = human?.ok ? ` / human: ${human.modernCities || 0} cities, ${human.ports || 0} ports, ${human.villages || 0} villages, ${(human.roadConnectorsCreated || 0) + (human.railwayConnectorsCreated || 0)} connectors, admin ${human.adminCellsReassigned || 0}, invalid ports ${human.invalidPortsRemoved || 0}` : "";
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch generated: ${result.label} / core ${formatRectSize(result.rects.coreRect)} / write ${formatRectSize(result.rects.writeRect)} / terrain ${result.updatedCells.toLocaleString()} cells / coast ${result.coastCellsChanged || 0} / natural ${result.naturalRegionsUpdated || 0}${humanText}`;
|
||||
renderTimingRows([]);
|
||||
window.setTimeout(() => setProgressVisible(false), 900);
|
||||
} catch (error) {
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch failed: ${error?.message || error}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function redraw(options = {}) {
|
||||
if (!state.world) return;
|
||||
state.camera = clampCameraToWorld(state.camera, state.world, MAP_W, MAP_H);
|
||||
state.viewportMap = getViewportMap(state.world, state.camera, MAP_W, MAP_H);
|
||||
state.hoverEntities = buildHoverEntities(state.viewportMap);
|
||||
drawMap(canvas, state.viewportMap, {
|
||||
mode: state.mode,
|
||||
showFeatures: state.showFeatures,
|
||||
showLabels: state.showLabels,
|
||||
showLabels: state.showLabels && !options.fastTerrain,
|
||||
continuousTerrain: !options.fastTerrain,
|
||||
});
|
||||
if (state.selectionRect && dragState.mode !== "select") updateSelectionOverlayFromWorldRect();
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
|
@ -373,6 +631,8 @@ function init() {
|
|||
});
|
||||
|
||||
generationTypeInput?.addEventListener("change", regenerate);
|
||||
patchTerrainTypeInput?.addEventListener("change", updatePatchControls);
|
||||
generatePatchButton?.addEventListener("click", generateSelectedPatch);
|
||||
|
||||
randomSeedButton.addEventListener("click", () => {
|
||||
seedInput.value = String(Math.floor(Math.random() * 9999999));
|
||||
|
|
@ -390,13 +650,17 @@ function init() {
|
|||
});
|
||||
|
||||
canvasShell?.setAttribute("tabindex", "0");
|
||||
window.addEventListener("keydown", handlePanKeyDown);
|
||||
window.addEventListener("keyup", handlePanKeyUp);
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
canvas.addEventListener("pointerdown", handleMapPointerDown);
|
||||
canvas.addEventListener("pointermove", handleMapPointerMove);
|
||||
canvas.addEventListener("pointerup", handleMapPointerUp);
|
||||
canvas.addEventListener("pointercancel", handleMapPointerUp);
|
||||
canvas.addEventListener("mousemove", updateTooltip);
|
||||
canvas.addEventListener("mouseleave", () => {
|
||||
tooltipEl?.classList.remove("visible");
|
||||
});
|
||||
|
||||
updatePatchControls();
|
||||
regenerate();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue