hmm?
This commit is contained in:
parent
ea0067dc0a
commit
112e6bf86b
11 changed files with 1586 additions and 1253 deletions
273
app.js
273
app.js
|
|
@ -2,7 +2,7 @@ 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 { clampCameraToWorld, createInitialCamera, createWorldMap, ensureWorldPaddingForCamera } from "./worldMap.js";
|
||||
import { getViewportMap } from "./worldViewport.js";
|
||||
import { PATCH_MIN_AREA, PATCH_MIN_HEIGHT, PATCH_MIN_WIDTH, buildPatchRects, generatePatch, validatePatchRect } from "./mapPatch.js";
|
||||
|
||||
|
|
@ -30,6 +30,8 @@ const state = {
|
|||
viewportMap: null,
|
||||
hoverEntities: [],
|
||||
selectionRect: null,
|
||||
patchVariant: 0,
|
||||
zoom: 1,
|
||||
lastPatchResult: null,
|
||||
};
|
||||
|
||||
|
|
@ -38,7 +40,9 @@ const canvasShell = document.querySelector(".canvas-shell");
|
|||
const seedInput = document.getElementById("seed");
|
||||
const generationTypeInput = document.getElementById("generationType");
|
||||
const patchTerrainTypeInput = document.getElementById("patchTerrainType");
|
||||
const patchVariantInput = document.getElementById("patchVariant");
|
||||
const generatePatchButton = document.getElementById("generatePatch");
|
||||
const alternativePatchButton = document.getElementById("alternativePatch");
|
||||
const patchStatusEl = document.getElementById("patchStatus");
|
||||
const randomSeedButton = document.getElementById("randomSeed");
|
||||
const showFeaturesInput = document.getElementById("showFeatures");
|
||||
|
|
@ -71,16 +75,72 @@ function activeMap() {
|
|||
return state.viewportMap || state.map;
|
||||
}
|
||||
|
||||
function clampZoom(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 1;
|
||||
return Math.min(Math.max(parsed, 0.55), 2.8);
|
||||
}
|
||||
|
||||
function zoomTransform() {
|
||||
const zoom = clampZoom(state.zoom || 1);
|
||||
const width = canvas.width || MAP_W * CELL_SIZE;
|
||||
const height = canvas.height || MAP_H * CELL_SIZE;
|
||||
return {
|
||||
zoom,
|
||||
width,
|
||||
height,
|
||||
tx: width * (1 - zoom) * 0.5,
|
||||
ty: height * (1 - zoom) * 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
function applyCanvasZoom() {
|
||||
if (!canvas) return;
|
||||
state.zoom = clampZoom(state.zoom || 1);
|
||||
// Keep the canvas element at a stable size. Zoom is applied inside the
|
||||
// renderer transform, not by resizing the scrollable shell.
|
||||
canvas.style.width = `${MAP_W * CELL_SIZE}px`;
|
||||
canvas.style.height = `${MAP_H * CELL_SIZE}px`;
|
||||
updateSelectionOverlayFromWorldRect();
|
||||
}
|
||||
|
||||
function displayedCellSize() {
|
||||
return CELL_SIZE * clampZoom(state.zoom || 1);
|
||||
}
|
||||
|
||||
function screenPointToMapPixel(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) return null;
|
||||
const t = zoomTransform();
|
||||
const scaleX = t.width / rect.width;
|
||||
const scaleY = t.height / rect.height;
|
||||
const canvasX = (clientX - rect.left) * scaleX;
|
||||
const canvasY = (clientY - rect.top) * scaleY;
|
||||
return {
|
||||
x: (canvasX - t.tx) / t.zoom,
|
||||
y: (canvasY - t.ty) / t.zoom,
|
||||
};
|
||||
}
|
||||
|
||||
function mapPixelToScreenPoint(px, py) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const t = zoomTransform();
|
||||
const canvasX = t.tx + px * t.zoom;
|
||||
const canvasY = t.ty + py * t.zoom;
|
||||
return {
|
||||
x: canvas.offsetLeft + canvasX * (rect.width / Math.max(1, t.width)),
|
||||
y: canvas.offsetTop + canvasY * (rect.height / Math.max(1, t.height)),
|
||||
};
|
||||
}
|
||||
|
||||
function mapClientToCell(event) {
|
||||
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;
|
||||
const p = screenPointToMapPixel(event.clientX, event.clientY);
|
||||
if (!p) return null;
|
||||
return {
|
||||
x: Math.floor(relX * map.width),
|
||||
y: Math.floor(relY * map.height),
|
||||
x: Math.floor(p.x / CELL_SIZE),
|
||||
y: Math.floor(p.y / CELL_SIZE),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +175,7 @@ function updateSelectionOverlay() {
|
|||
const validation = validatePatchRect(liveRect, state.world);
|
||||
selectionEl.classList.toggle("invalid", !validation.ok);
|
||||
if (generatePatchButton) generatePatchButton.disabled = true;
|
||||
if (alternativePatchButton) alternativePatchButton.disabled = true;
|
||||
if (patchStatusEl) {
|
||||
const current = validation.rect || liveRect;
|
||||
patchStatusEl.textContent = validation.ok
|
||||
|
|
@ -128,13 +189,14 @@ 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 p0 = mapPixelToScreenPoint((state.selectionRect.x0 - cameraX) * CELL_SIZE, (state.selectionRect.y0 - cameraY) * CELL_SIZE);
|
||||
const p1 = mapPixelToScreenPoint((state.selectionRect.x1 - cameraX) * CELL_SIZE, (state.selectionRect.y1 - cameraY) * CELL_SIZE);
|
||||
const vx0 = p0.x - canvas.offsetLeft;
|
||||
const vy0 = p0.y - canvas.offsetTop;
|
||||
const vx1 = p1.x - canvas.offsetLeft;
|
||||
const vy1 = p1.y - canvas.offsetTop;
|
||||
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);
|
||||
|
|
@ -159,13 +221,37 @@ function formatRectSize(rect) {
|
|||
return `${w} x ${h} cells / ${(w * h).toLocaleString()} cells`;
|
||||
}
|
||||
|
||||
function normalizePatchVariant(value) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) >>> 0 : 0;
|
||||
}
|
||||
|
||||
function setPatchVariant(value, { update = true } = {}) {
|
||||
state.patchVariant = normalizePatchVariant(value);
|
||||
if (patchVariantInput && patchVariantInput.value !== String(state.patchVariant)) {
|
||||
patchVariantInput.value = String(state.patchVariant);
|
||||
}
|
||||
if (update) updatePatchControls();
|
||||
return state.patchVariant;
|
||||
}
|
||||
|
||||
function readPatchVariant() {
|
||||
return setPatchVariant(patchVariantInput?.value ?? state.patchVariant, { update: false });
|
||||
}
|
||||
|
||||
function resetPatchVariant({ update = true } = {}) {
|
||||
return setPatchVariant(0, { update });
|
||||
}
|
||||
|
||||
function updatePatchControls() {
|
||||
if (!patchStatusEl && !generatePatchButton) return;
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
const variant = readPatchVariant();
|
||||
if (generatePatchButton) generatePatchButton.disabled = !validation.ok;
|
||||
if (alternativePatchButton) alternativePatchButton.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.textContent = `Right-drag an area. Minimum: ${PATCH_MIN_WIDTH} x ${PATCH_MIN_HEIGHT} cells and ${PATCH_MIN_AREA.toLocaleString()} cells. Current variant: ${variant}.`;
|
||||
patchStatusEl.classList.toggle("invalid", false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -176,9 +262,9 @@ function updatePatchControls() {
|
|||
}
|
||||
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}` : ""}.`
|
||||
? ` Last patch: ${state.lastPatchResult.label}, variant ${state.lastPatchResult.variant ?? "-"}, mode ${state.lastPatchResult.patchGenerationMode || "legacy-full-pipeline"}, 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}, idmap ${state.lastPatchResult.humanGeography.continuityIdMappings || 0}/${state.lastPatchResult.humanGeography.continuityIdMappedCells || 0}, invalid ports ${state.lastPatchResult.humanGeography.invalidPortsRemoved || 0}` : ""}.`
|
||||
: "";
|
||||
patchStatusEl.textContent = `Core: ${formatRectSize(rects.coreRect)}. Write: ${formatRectSize(rects.writeRect)}. Repair: ${formatRectSize(rects.repairRect)}.${patchText}`;
|
||||
patchStatusEl.textContent = `Variant: ${variant}. Core: ${formatRectSize(rects.coreRect)}. Write: ${formatRectSize(rects.writeRect)}. Context: ${formatRectSize(rects.contextRect)}. Repair: ${formatRectSize(rects.repairRect)}.${patchText}`;
|
||||
patchStatusEl.classList.toggle("invalid", false);
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +297,7 @@ function hideSelectionOverlay() {
|
|||
dragState.selectStart = null;
|
||||
dragState.selectEnd = null;
|
||||
state.selectionRect = null;
|
||||
resetPatchVariant({ update: false });
|
||||
if (selectionEl) selectionEl.style.display = "none";
|
||||
updatePatchControls();
|
||||
}
|
||||
|
|
@ -220,10 +307,18 @@ function selectionPixelsToCells(start, end) {
|
|||
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 toMapPixel = (p) => {
|
||||
const t = zoomTransform();
|
||||
const canvasX = p.x * (t.width / rect.width);
|
||||
const canvasY = p.y * (t.height / rect.height);
|
||||
return { x: (canvasX - t.tx) / t.zoom, y: (canvasY - t.ty) / t.zoom };
|
||||
};
|
||||
const a = toMapPixel(start);
|
||||
const b = toMapPixel(end);
|
||||
const localX0 = Math.floor(Math.min(a.x, b.x) / CELL_SIZE);
|
||||
const localY0 = Math.floor(Math.min(a.y, b.y) / CELL_SIZE);
|
||||
const localX1 = Math.ceil(Math.max(a.x, b.x) / CELL_SIZE);
|
||||
const localY1 = Math.ceil(Math.max(a.y, b.y) / CELL_SIZE);
|
||||
const cameraX = Math.round(state.camera?.x || 0);
|
||||
const cameraY = Math.round(state.camera?.y || 0);
|
||||
return {
|
||||
|
|
@ -264,8 +359,9 @@ function handleMapPointerMove(event) {
|
|||
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 cellSize = Math.max(1, displayedCellSize());
|
||||
const dxCells = Math.round((event.clientX - dragState.startClientX) / cellSize);
|
||||
const dyCells = Math.round((event.clientY - dragState.startClientY) / cellSize);
|
||||
const nextCamera = clampCameraToWorld({
|
||||
x: dragState.startCameraX - dxCells,
|
||||
y: dragState.startCameraY - dyCells,
|
||||
|
|
@ -288,6 +384,8 @@ function handleMapPointerUp(event) {
|
|||
const height = Math.abs(dragState.selectEnd.y - dragState.selectStart.y);
|
||||
if (width >= 4 && height >= 4) {
|
||||
state.selectionRect = selectionPixelsToCells(dragState.selectStart, dragState.selectEnd);
|
||||
state.lastPatchResult = null;
|
||||
resetPatchVariant({ update: false });
|
||||
updateSelectionOverlayFromWorldRect();
|
||||
updatePatchControls();
|
||||
} else {
|
||||
|
|
@ -466,20 +564,61 @@ function landuseName(value) {
|
|||
return landuseLabel(value);
|
||||
}
|
||||
|
||||
function adminName(map, adminId) {
|
||||
const center = (map.adminCenters || [])[adminId];
|
||||
return center?.name || (adminId >= 0 ? `Municipality ${adminId + 1}` : "-");
|
||||
function numericIdOf(item) {
|
||||
for (const key of ["adminId", "municipalityId", "id", "adminNumericId"]) {
|
||||
const value = item?.[key];
|
||||
if (Number.isFinite(value)) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function adminPopulation(map, adminId) {
|
||||
const center = (map.adminCenters || [])[adminId];
|
||||
function adminCenterForId(map, adminId) {
|
||||
if (!map || adminId == null || adminId < 0) return null;
|
||||
const centers = map.adminCenters || [];
|
||||
const direct = centers[adminId];
|
||||
if (direct && [direct.adminId, direct.municipalityId, direct.id, direct.adminNumericId].some((v) => v === adminId)) return direct;
|
||||
return centers.find((center) => [center?.adminId, center?.municipalityId, center?.id, center?.adminNumericId].some((v) => v === adminId)) || null;
|
||||
}
|
||||
|
||||
function looksNumericName(name) {
|
||||
if (!name) return true;
|
||||
const text = String(name).trim();
|
||||
return !text || /^-?\d+(?:\s*[,,]\s*\d+)*$/u.test(text) || /^(Municipality|Prefecture)\s+-?\d+/i.test(text);
|
||||
}
|
||||
|
||||
function nearestNamedAdminCenter(map, cellIndex, maxDistance = 36) {
|
||||
if (!map || cellIndex < 0) return null;
|
||||
const x = cellIndex % map.width;
|
||||
const y = Math.floor(cellIndex / map.width);
|
||||
let best = null;
|
||||
let bestD = maxDistance;
|
||||
for (const center of map.adminCenters || []) {
|
||||
const name = center?.name || center?.municipalityName || center?.canonicalSettlementName || center?.municipalityRootName || center?.generatedMunicipalityName;
|
||||
if (looksNumericName(name) || !Number.isFinite(center?.x) || !Number.isFinite(center?.y)) continue;
|
||||
const d = Math.hypot(center.x - x, center.y - y);
|
||||
if (d < bestD) { best = center; bestD = d; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function adminName(map, adminId, cellIndex = -1) {
|
||||
const center = adminCenterForId(map, adminId) || nearestNamedAdminCenter(map, cellIndex);
|
||||
const name = center?.municipalityName || center?.name || center?.canonicalSettlementName || center?.municipalityRootName || center?.generatedMunicipalityName;
|
||||
if (!looksNumericName(name)) return name;
|
||||
return adminId >= 0 ? "Unnamed municipality" : "-";
|
||||
}
|
||||
|
||||
function adminPopulation(map, adminId, cellIndex = -1) {
|
||||
const center = adminCenterForId(map, adminId) || nearestNamedAdminCenter(map, cellIndex);
|
||||
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}` : "-");
|
||||
if (region?.name && !looksNumericName(region.name)) return region.name;
|
||||
const center = nearestNamedAdminCenter(map, i, 80);
|
||||
return center?.prefectureName || center?.prefectureRegionName || (id >= 0 ? "Unnamed prefecture" : "-");
|
||||
}
|
||||
|
||||
function updateTooltip(event) {
|
||||
|
|
@ -497,9 +636,9 @@ function updateTooltip(event) {
|
|||
const worldCell = viewportCellToWorldCell({ x, y });
|
||||
const entity = nearestEntity(state.hoverEntities, x, y);
|
||||
const elevation = map.elevation?.[i] ?? 0;
|
||||
const density = map.populationDensity?.[i] ?? 0;
|
||||
const density = map.populationDensity?.[i] ?? map.settlementScore?.[i] ?? 0;
|
||||
const hoveredAdminId = map.adminId?.[i] ?? -1;
|
||||
const hoveredAdminPopulation = adminPopulation(map, hoveredAdminId);
|
||||
const hoveredAdminPopulation = adminPopulation(map, hoveredAdminId, i);
|
||||
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"}`
|
||||
|
|
@ -507,7 +646,7 @@ function updateTooltip(event) {
|
|||
const lines = [
|
||||
`<strong>${entityTitle}</strong>`,
|
||||
`Prefecture: ${prefectureNameForCell(map, i)}`,
|
||||
`Admin: ${adminName(map, hoveredAdminId)}`,
|
||||
`Admin: ${adminName(map, hoveredAdminId, i)}`,
|
||||
`Admin Pop: ${hoveredAdminPopulation === null ? "-" : hoveredAdminPopulation.toLocaleString()}`,
|
||||
`Land: ${map.sea?.[i] ? "Sea" : landuseName(map.landuse?.[i])}`,
|
||||
`Elevation: ${elevation.toFixed(3)} / Slope: ${(map.slope?.[i] ?? 0).toFixed(3)}`,
|
||||
|
|
@ -552,6 +691,7 @@ async function regenerate() {
|
|||
state.world = createWorldMap(state.map);
|
||||
state.camera = createInitialCamera(state.world);
|
||||
state.lastPatchResult = null;
|
||||
resetPatchVariant({ update: false });
|
||||
hideSelectionOverlay();
|
||||
renderStats(state.map);
|
||||
redraw();
|
||||
|
|
@ -565,16 +705,41 @@ async function regenerate() {
|
|||
}
|
||||
|
||||
|
||||
function derivePatchSeed(rect, terrainType) {
|
||||
function derivePatchSeed(rect, terrainType, variant = 0) {
|
||||
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;
|
||||
h = Math.imul(h ^ normalizePatchVariant(variant), 668265263) >>> 0;
|
||||
for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
|
||||
function handleCanvasWheel(event) {
|
||||
if (!state.world || !activeMap()) return;
|
||||
event.preventDefault();
|
||||
tooltipEl?.classList.remove("visible");
|
||||
const beforeCell = mapClientToCell(event);
|
||||
const beforeWorld = beforeCell ? viewportCellToWorldCell(beforeCell) : null;
|
||||
const oldZoom = clampZoom(state.zoom || 1);
|
||||
const delta = event.deltaY < 0 ? 1.12 : 1 / 1.12;
|
||||
const nextZoom = clampZoom(oldZoom * delta);
|
||||
if (Math.abs(nextZoom - oldZoom) < 0.001) return;
|
||||
state.zoom = nextZoom;
|
||||
if (beforeWorld) {
|
||||
const afterCell = mapClientToCell(event);
|
||||
if (afterCell) {
|
||||
state.camera = clampCameraToWorld({
|
||||
x: beforeWorld.x - afterCell.x,
|
||||
y: beforeWorld.y - afterCell.y,
|
||||
}, state.world, MAP_W, MAP_H);
|
||||
}
|
||||
}
|
||||
redraw({ fastTerrain: false });
|
||||
}
|
||||
|
||||
async function generateSelectedPatch() {
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
if (!validation.ok) {
|
||||
|
|
@ -582,11 +747,12 @@ async function generateSelectedPatch() {
|
|||
return;
|
||||
}
|
||||
const terrainType = patchTerrainTypeInput?.value || generationTypeInput?.value || "auto";
|
||||
const seed = derivePatchSeed(validation.rect, terrainType);
|
||||
const variant = readPatchVariant();
|
||||
const seed = derivePatchSeed(validation.rect, terrainType, variant);
|
||||
setProgressVisible(true, "Generating selected patch...");
|
||||
await nextFrame();
|
||||
try {
|
||||
const result = generatePatch(state.world, validation.rect, { terrainType, seed });
|
||||
const result = generatePatch(state.world, validation.rect, { terrainType, seed, variant });
|
||||
if (!result.ok) {
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch failed: ${result.reason || "invalid selection"}`;
|
||||
updatePatchControls();
|
||||
|
|
@ -599,7 +765,7 @@ async function generateSelectedPatch() {
|
|||
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}`;
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch generated: ${result.label} / variant ${result.variant ?? variant} / mode ${result.patchGenerationMode || "legacy-full-pipeline"} / 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) {
|
||||
|
|
@ -608,8 +774,30 @@ async function generateSelectedPatch() {
|
|||
}
|
||||
}
|
||||
|
||||
async function generateAlternativePatch() {
|
||||
const validation = validatePatchRect(state.selectionRect, state.world);
|
||||
if (!validation.ok) {
|
||||
updatePatchControls();
|
||||
return;
|
||||
}
|
||||
setPatchVariant(readPatchVariant() + 1, { update: false });
|
||||
await generateSelectedPatch();
|
||||
}
|
||||
|
||||
function redraw(options = {}) {
|
||||
if (!state.world) return;
|
||||
const expansion = ensureWorldPaddingForCamera(state.world, state.camera, MAP_W, MAP_H);
|
||||
if (expansion?.expanded) {
|
||||
state.camera = { x: (state.camera?.x || 0) + (expansion.dx || 0), y: (state.camera?.y || 0) + (expansion.dy || 0) };
|
||||
if (state.selectionRect) {
|
||||
state.selectionRect = {
|
||||
x0: state.selectionRect.x0 + (expansion.dx || 0),
|
||||
y0: state.selectionRect.y0 + (expansion.dy || 0),
|
||||
x1: state.selectionRect.x1 + (expansion.dx || 0),
|
||||
y1: state.selectionRect.y1 + (expansion.dy || 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
|
@ -618,7 +806,9 @@ function redraw(options = {}) {
|
|||
showFeatures: state.showFeatures,
|
||||
showLabels: state.showLabels && !options.fastTerrain,
|
||||
continuousTerrain: !options.fastTerrain,
|
||||
zoom: state.zoom || 1,
|
||||
});
|
||||
applyCanvasZoom();
|
||||
if (state.selectionRect && dragState.mode !== "select") updateSelectionOverlayFromWorldRect();
|
||||
}
|
||||
|
||||
|
|
@ -631,8 +821,20 @@ function init() {
|
|||
});
|
||||
|
||||
generationTypeInput?.addEventListener("change", regenerate);
|
||||
patchTerrainTypeInput?.addEventListener("change", updatePatchControls);
|
||||
patchTerrainTypeInput?.addEventListener("change", () => {
|
||||
state.lastPatchResult = null;
|
||||
resetPatchVariant({ update: false });
|
||||
updatePatchControls();
|
||||
});
|
||||
patchVariantInput?.addEventListener("change", () => setPatchVariant(patchVariantInput.value));
|
||||
patchVariantInput?.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
setPatchVariant(patchVariantInput.value);
|
||||
generateSelectedPatch();
|
||||
}
|
||||
});
|
||||
generatePatchButton?.addEventListener("click", generateSelectedPatch);
|
||||
alternativePatchButton?.addEventListener("click", generateAlternativePatch);
|
||||
|
||||
randomSeedButton.addEventListener("click", () => {
|
||||
seedInput.value = String(Math.floor(Math.random() * 9999999));
|
||||
|
|
@ -651,6 +853,7 @@ function init() {
|
|||
|
||||
canvasShell?.setAttribute("tabindex", "0");
|
||||
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
|
||||
canvas.addEventListener("pointerdown", handleMapPointerDown);
|
||||
canvas.addEventListener("pointermove", handleMapPointerMove);
|
||||
canvas.addEventListener("pointerup", handleMapPointerUp);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue