/* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck import type { WorldshaperStudioBootstrap } from "../worldshaperStudio/bootstrap"; import { loadWorldshaperStudioBootstrap, loadStandaloneWorldshaperBootstrap, } from "../worldshaperStudio/bootstrap"; import { persistWorldshaperHeightViewerBounds } from "../worldshaperStudio/windowing"; import { createDebouncedCallback } from "../worldshaperStudio/debounce"; const VIEWER_STYLE_ID = "worldshaper-height-viewer-styles"; function ensureStyles(): void { let styleEl = document.getElementById(VIEWER_STYLE_ID) as HTMLStyleElement | null; if (!styleEl) { styleEl = document.createElement("style"); styleEl.id = VIEWER_STYLE_ID; document.head.appendChild(styleEl); } styleEl.textContent = ` :root { color-scheme: dark; } * { box-sizing: border-box; } html, body { margin: 0; width: 100%; height: 100%; background: #07111f; color: #d8e8ff; font-family: Segoe UI, Arial, sans-serif; } .viewer-shell { display: grid; grid-template-rows: 52px 1fr; width: 100vw; height: 100vh; } .viewer-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid #274472; background: linear-gradient(180deg, #152645 0%, #0d1b33 100%); } .viewer-title { min-width: 0; display: grid; gap: 2px; } .viewer-title strong { font-size: 14px; color: #eef6ff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .viewer-title span { font-size: 11px; color: #9fb8e5; } .viewer-controls { display: inline-flex; align-items: center; gap: 8px; } .viewer-btn { height: 34px; padding: 0 12px; border: 1px solid #3c5e95; border-radius: 8px; background: #1a345e; color: #d6e7ff; font-size: 12px; font-weight: 700; cursor: pointer; } .viewer-btn:hover { background: #214679; } .viewer-btn:disabled { opacity: 0.55; cursor: not-allowed; background: #132643; } .viewer-height-pill { min-width: 96px; height: 34px; padding: 0 12px; border: 1px solid #3c5e95; border-radius: 999px; background: #10284b; color: #d6e7ff; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; white-space: nowrap; } .viewer-hint { font-size: 11px; color: #9fb8e5; white-space: nowrap; } .viewer-viewport { position: relative; min-width: 0; min-height: 0; overflow: auto; background: linear-gradient(180deg, rgba(20, 38, 69, 0.16), rgba(7, 17, 31, 0.04)), #07111f; } .viewer-viewport-layer { position: sticky; top: 0; left: 0; width: 100%; height: 0; overflow: visible; z-index: 1; pointer-events: none; } .viewer-viewport-layer canvas { display: block; pointer-events: none; image-rendering: pixelated; image-rendering: crisp-edges; } .viewer-viewport-spacer { position: relative; z-index: 0; pointer-events: none; } `; } function renderMessage(title: string, message: string): void { document.body.innerHTML = ""; document.body.style.margin = "0"; document.body.style.minHeight = "100vh"; document.body.style.display = "grid"; document.body.style.placeItems = "center"; document.body.style.background = "#07111f"; document.body.style.color = "#d8e8ff"; document.body.style.fontFamily = "Segoe UI, Arial, sans-serif"; const panel = document.createElement("div"); panel.style.maxWidth = "460px"; panel.style.padding = "24px"; panel.style.border = "1px solid #2e426c"; panel.style.borderRadius = "10px"; panel.style.background = "#0e1a33"; panel.style.boxShadow = "0 12px 36px rgba(3, 8, 18, 0.45)"; const heading = document.createElement("h1"); heading.textContent = title; heading.style.margin = "0 0 8px"; heading.style.fontSize = "18px"; const text = document.createElement("p"); text.textContent = message; text.style.margin = "0"; text.style.fontSize = "14px"; text.style.lineHeight = "1.5"; panel.appendChild(heading); panel.appendChild(text); document.body.appendChild(panel); } function renderLoading(message: string): void { renderMessage("Loading Worldshaper Height Viewer", message); } function renderError(message: string): void { renderMessage("Worldshaper Height Viewer unavailable", message); } function cloneValue(value: T): T { if (typeof structuredClone === "function") { return structuredClone(value); } return value == null ? value : JSON.parse(JSON.stringify(value)); } function buildViewerMarkup(): string { return `
Worldshaper Height Viewer Previewing current world snapshot.
Height 0
Use Up / Down arrows to change height.
`; } function startViewer(bootstrap: WorldshaperStudioBootstrap): void { document.body.removeAttribute("style"); document.body.innerHTML = buildViewerMarkup(); document.title = "Worldshaper Height Viewer - " + (bootstrap.mapName || bootstrap.mapId || "Untitled"); const titleEl = document.getElementById("viewerTitle"); const metaEl = document.getElementById("viewerMeta"); const heightLabelEl = document.getElementById("heightLabel"); const heightDownBtn = document.getElementById("heightDownBtn") as HTMLButtonElement | null; const heightUpBtn = document.getElementById("heightUpBtn") as HTMLButtonElement | null; const viewportEl = document.getElementById("viewerViewport") as HTMLDivElement | null; const viewportSpacerEl = document.getElementById("viewerViewportSpacer") as HTMLDivElement | null; const canvasEl = document.getElementById("viewerCanvas") as HTMLCanvasElement | null; const ctx = canvasEl?.getContext("2d") || null; if (!viewportEl || !viewportSpacerEl || !canvasEl || !ctx) { renderError("The height viewer could not initialize its canvas."); return; } const mapWidth = Math.max(1, Number(bootstrap.width) || 1); const mapHeight = Math.max(1, Number(bootstrap.height) || 1); const tileSize = Math.max(8, Number(bootstrap.tileSize) || 32); const worldPixelWidth = mapWidth * tileSize; const worldPixelHeight = mapHeight * tileSize; const backgroundColor = /^#[0-9a-fA-F]{6}$/.test(String(bootstrap.backgroundColor || "").trim()) ? String(bootstrap.backgroundColor).trim().toUpperCase() : "#060A14"; const layers = Array.isArray(bootstrap.roomLayers) ? cloneValue(bootstrap.roomLayers).map((layer) => ({ layer: Number(layer.layer) || 0, name: typeof layer.name === "string" ? layer.name.trim() : "", rows: Array.isArray(layer.rows) ? layer.rows.map((row) => String(row || "")) : [], })).sort((a, b) => a.layer - b.layer) : []; const heightLayers = Array.isArray(bootstrap.heightLayers) ? cloneValue(bootstrap.heightLayers).map((entry, index) => { const rows = Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "").replace(/\./g, " ")) : []; const width = rows.reduce((max, row) => Math.max(max, row.length), 0); const height = rows.length; const x = Math.max(0, Number(entry?.x) || 0); const y = Math.max(0, Number(entry?.y) || 0); return { id: String(entry?.id || ("height_" + String(index + 1))).trim() || ("height_" + String(index + 1)), name: typeof entry?.name === "string" ? entry.name.trim() : "", z: Math.max(1, Math.floor(Number(entry?.z) || 1)), x, y, rows, width, height, pixelX: x * tileSize, pixelY: y * tileSize, pixelWidth: width * tileSize, pixelHeight: height * tileSize, }; }) : []; const heightLayersByZ = new Map>>(); heightLayers.forEach((entry) => { const z = Math.max(1, Number(entry.z) || 1); if (!heightLayersByZ.has(z)) { heightLayersByZ.set(z, []); } heightLayersByZ.get(z)?.push(entry); }); const maxHeight = heightLayers.reduce((max, entry) => Math.max(max, Math.max(1, Number(entry?.z) || 1)), 0); const tileCatalogBySymbol: Record = {}; Object.entries(bootstrap.tileColors || {}).forEach(([symbol, color]) => { tileCatalogBySymbol[symbol] = { symbol, color: String(color || "#7AA7FF"), dataUrl: null, }; }); Object.values(bootstrap.tileCatalogById || {}).forEach((entry) => { const symbol = String(entry?.symbol || "").charAt(0); if (!symbol) { return; } tileCatalogBySymbol[symbol] = { symbol, color: String(entry?.color || tileCatalogBySymbol[symbol]?.color || "#7AA7FF"), dataUrl: entry?.dataUrl || null, }; }); const backgroundTileId = String(bootstrap.backgroundTileId || "").trim(); const backgroundSymbol = backgroundTileId ? String(bootstrap.tileCatalogById?.[backgroundTileId]?.symbol || ".").charAt(0) || "." : ""; const imageCache: Record = {}; const patchSurfaceCache = new Map(); const baseSurfaceCanvas = document.createElement("canvas"); const baseSurfaceCtx = baseSurfaceCanvas.getContext("2d"); const baseSurfaceState = { dirty: true, width: 0, height: 0, viewportLeft: -1, viewportTop: -1, tileSize: 0, }; const state = { currentHeight: 0, pendingDrawFrame: 0, }; const heightBlurStep = Math.max(0, Math.min(1, Number(bootstrap.heightBlurStep ?? bootstrap.heightDetailStep) || 0.1)); function clampViewerHeight(value: unknown): number { return Math.max(0, Math.min(maxHeight, Number(value) || 0)); } function getHeightBlurStrength(height: number): number { const normalizedHeight = Math.max(0, Number(height) || 0); return Math.min(8, normalizedHeight * heightBlurStep * (tileSize / 4)); } function syncViewportDimensions(): void { const nextCanvasWidth = Math.max(1, Math.ceil(Number(viewportEl.clientWidth) || 0)); const nextCanvasHeight = Math.max(1, Math.ceil(Number(viewportEl.clientHeight) || 0)); if (canvasEl.width !== nextCanvasWidth || canvasEl.height !== nextCanvasHeight) { canvasEl.width = nextCanvasWidth; canvasEl.height = nextCanvasHeight; } canvasEl.style.width = nextCanvasWidth + "px"; canvasEl.style.height = nextCanvasHeight + "px"; viewportSpacerEl.style.width = Math.max(nextCanvasWidth, worldPixelWidth) + "px"; viewportSpacerEl.style.height = Math.max(nextCanvasHeight, worldPixelHeight) + "px"; } function getViewportRenderRect() { const viewportWidth = Math.max(1, Math.ceil(Number(viewportEl.clientWidth) || 0)); const viewportHeight = Math.max(1, Math.ceil(Number(viewportEl.clientHeight) || 0)); const left = Math.max(0, Math.min(worldPixelWidth, Math.floor(Number(viewportEl.scrollLeft) || 0))); const top = Math.max(0, Math.min(worldPixelHeight, Math.floor(Number(viewportEl.scrollTop) || 0))); const right = Math.max(left + 1, Math.min(worldPixelWidth, Math.ceil((Number(viewportEl.scrollLeft) || 0) + viewportWidth))); const bottom = Math.max(top + 1, Math.min(worldPixelHeight, Math.ceil((Number(viewportEl.scrollTop) || 0) + viewportHeight))); return { left, top, right, bottom, width: right - left, height: bottom - top, }; } function rectIntersects(rect, x, y, width, height): boolean { return x + width > rect.left && x < rect.right && y + height > rect.top && y < rect.bottom; } function updateHeightLabel(): void { if (heightLabelEl) { heightLabelEl.textContent = "Height " + state.currentHeight; } if (heightDownBtn) { heightDownBtn.disabled = state.currentHeight <= 0; } if (heightUpBtn) { heightUpBtn.disabled = state.currentHeight >= maxHeight; } } function invalidateBaseSurface(): void { baseSurfaceState.dirty = true; baseSurfaceState.viewportLeft = -1; baseSurfaceState.viewportTop = -1; baseSurfaceState.tileSize = 0; } function drawSymbolAtPixel(targetCtx: CanvasRenderingContext2D, symbol: string, drawX: number, drawY: number): void { const tileEntry = tileCatalogBySymbol[symbol] || tileCatalogBySymbol["."] || { color: "#7AA7FF", dataUrl: null }; const img = getTileImage(symbol); if (img && img.complete && img.naturalWidth > 0) { targetCtx.drawImage(img, drawX, drawY, tileSize, tileSize); return; } targetCtx.fillStyle = tileEntry.color || "#7AA7FF"; targetCtx.fillRect(drawX, drawY, tileSize, tileSize); } function getTileImage(symbol: string): HTMLImageElement | null { const tileEntry = tileCatalogBySymbol[symbol]; if (!tileEntry?.dataUrl) { return null; } if (imageCache[symbol]) { return imageCache[symbol]; } const img = new Image(); img.src = tileEntry.dataUrl; img.onload = () => { patchSurfaceCache.clear(); invalidateBaseSurface(); draw(); }; imageCache[symbol] = img; return img; } function drawVisibleBaseTiles(targetCtx: CanvasRenderingContext2D, viewportRect): void { const startTileX = Math.max(0, Math.floor(viewportRect.left / tileSize)); const endTileX = Math.min(mapWidth - 1, Math.ceil(viewportRect.right / tileSize)); const startTileY = Math.max(0, Math.floor(viewportRect.top / tileSize)); const endTileY = Math.min(mapHeight - 1, Math.ceil(viewportRect.bottom / tileSize)); targetCtx.save(); targetCtx.setTransform(1, 0, 0, 1, -viewportRect.left, -viewportRect.top); targetCtx.imageSmoothingEnabled = false; layers.forEach((layer) => { const isBackgroundLayer = (Number(layer.layer) || 0) === 0; const fillChar = isBackgroundLayer ? "." : " "; const rows = Array.isArray(layer.rows) ? layer.rows : []; for (let tileY = startTileY; tileY <= endTileY; tileY += 1) { const row = String(rows[tileY] || ""); for (let tileX = startTileX; tileX <= endTileX; tileX += 1) { let ch = row.charAt(tileX) || fillChar; if (isBackgroundLayer && ch === " ") { continue; } if (isBackgroundLayer && ch === "." && backgroundSymbol) { ch = backgroundSymbol; } if (!isBackgroundLayer && ch === " ") { continue; } if (ch === ".") { continue; } drawSymbolAtPixel(targetCtx, ch, tileX * tileSize, tileY * tileSize); } } }); targetCtx.restore(); } function baseSurfaceNeedsRefresh(viewportRect, canvasWidth: number, canvasHeight: number): boolean { if (baseSurfaceState.dirty || baseSurfaceState.width !== canvasWidth || baseSurfaceState.height !== canvasHeight) { return true; } if (baseSurfaceState.viewportLeft !== viewportRect.left || baseSurfaceState.viewportTop !== viewportRect.top) { return true; } if (baseSurfaceState.tileSize !== tileSize) { return true; } return false; } function refreshBaseSurface(viewportRect, canvasWidth: number, canvasHeight: number): void { if (!baseSurfaceCtx) { return; } if (baseSurfaceCanvas.width !== canvasWidth || baseSurfaceCanvas.height !== canvasHeight) { baseSurfaceCanvas.width = canvasWidth; baseSurfaceCanvas.height = canvasHeight; } baseSurfaceCtx.setTransform(1, 0, 0, 1, 0, 0); baseSurfaceCtx.clearRect(0, 0, canvasWidth, canvasHeight); drawVisibleBaseTiles(baseSurfaceCtx, viewportRect); baseSurfaceState.width = canvasWidth; baseSurfaceState.height = canvasHeight; baseSurfaceState.viewportLeft = viewportRect.left; baseSurfaceState.viewportTop = viewportRect.top; baseSurfaceState.tileSize = tileSize; baseSurfaceState.dirty = false; } function getOrBuildPatchSurface(entry): HTMLCanvasElement | null { if (!entry || entry.pixelWidth <= 0 || entry.pixelHeight <= 0) { return null; } const entryId = String(entry.id || "").trim(); if (!entryId) { return null; } const cached = patchSurfaceCache.get(entryId); if (cached) { return cached; } const surface = document.createElement("canvas"); surface.width = Math.max(1, Number(entry.pixelWidth) || 1); surface.height = Math.max(1, Number(entry.pixelHeight) || 1); const surfaceCtx = surface.getContext("2d"); if (!surfaceCtx) { return null; } surfaceCtx.imageSmoothingEnabled = false; const rows = Array.isArray(entry.rows) ? entry.rows : []; rows.forEach((rawRow, localY) => { const row = String(rawRow || ""); for (let localX = 0; localX < row.length; localX += 1) { const symbol = String(row.charAt(localX) || " ").charAt(0) || " "; if (symbol === " " || symbol === ".") { continue; } drawSymbolAtPixel(surfaceCtx, symbol, localX * tileSize, localY * tileSize); } }); patchSurfaceCache.set(entryId, surface); return surface; } function drawVisibleHeightPatches(viewportRect): void { const visibleEntries = heightLayersByZ.get(state.currentHeight) || []; if (visibleEntries.length <= 0) { return; } ctx.save(); ctx.imageSmoothingEnabled = false; visibleEntries.forEach((entry) => { const patchPixelX = Math.max(0, Number(entry.pixelX) || 0); const patchPixelY = Math.max(0, Number(entry.pixelY) || 0); const patchPixelWidth = Math.max(0, Number(entry.pixelWidth) || 0); const patchPixelHeight = Math.max(0, Number(entry.pixelHeight) || 0); if (patchPixelWidth <= 0 || patchPixelHeight <= 0) { return; } if (!rectIntersects(viewportRect, patchPixelX, patchPixelY, patchPixelWidth, patchPixelHeight)) { return; } const surface = getOrBuildPatchSurface(entry); if (!surface) { return; } const cropLeft = Math.max(viewportRect.left, patchPixelX); const cropTop = Math.max(viewportRect.top, patchPixelY); const cropRight = Math.min(viewportRect.right, patchPixelX + patchPixelWidth); const cropBottom = Math.min(viewportRect.bottom, patchPixelY + patchPixelHeight); const sourceX = cropLeft - patchPixelX; const sourceY = cropTop - patchPixelY; const drawWidth = cropRight - cropLeft; const drawHeight = cropBottom - cropTop; if (drawWidth <= 0 || drawHeight <= 0) { return; } ctx.drawImage( surface, sourceX, sourceY, drawWidth, drawHeight, cropLeft - viewportRect.left, cropTop - viewportRect.top, drawWidth, drawHeight, ); }); ctx.restore(); } function performDraw(): void { syncViewportDimensions(); const canvasWidth = Math.max(1, canvasEl.width || Math.ceil(Number(viewportEl.clientWidth) || 0)); const canvasHeight = Math.max(1, canvasEl.height || Math.ceil(Number(viewportEl.clientHeight) || 0)); const viewportRect = getViewportRenderRect(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvasWidth, canvasHeight); ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, canvasWidth, canvasHeight); if (baseSurfaceNeedsRefresh(viewportRect, canvasWidth, canvasHeight)) { refreshBaseSurface(viewportRect, canvasWidth, canvasHeight); } if (baseSurfaceCanvas.width > 0 && baseSurfaceCanvas.height > 0) { ctx.save(); if (state.currentHeight > 0) { const blurStrength = getHeightBlurStrength(state.currentHeight); ctx.globalAlpha = Math.max(0.95, 1 - (state.currentHeight * 0.01)); ctx.filter = blurStrength > 0 ? `blur(${blurStrength}px)` : "none"; ctx.imageSmoothingEnabled = blurStrength > 0; ctx.drawImage(baseSurfaceCanvas, 0, 0); } else { ctx.globalAlpha = 1; ctx.filter = "none"; ctx.imageSmoothingEnabled = false; ctx.drawImage(baseSurfaceCanvas, 0, 0); } ctx.restore(); if (state.currentHeight > 0) { ctx.save(); ctx.fillStyle = "rgba(7, 12, 20, 0.06)"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.restore(); } } drawVisibleHeightPatches(viewportRect); updateHeightLabel(); } function drawNow(): void { if (state.pendingDrawFrame) { window.cancelAnimationFrame(state.pendingDrawFrame); state.pendingDrawFrame = 0; } performDraw(); } function draw(): void { if (state.pendingDrawFrame) { return; } state.pendingDrawFrame = window.requestAnimationFrame(() => { state.pendingDrawFrame = 0; performDraw(); }); } function setHeight(nextHeight: number): void { const normalizedHeight = clampViewerHeight(nextHeight); if (normalizedHeight === state.currentHeight) { updateHeightLabel(); return; } state.currentHeight = normalizedHeight; draw(); } function changeHeight(delta: number): void { setHeight(state.currentHeight + (Number(delta) || 0)); } function handleWindowKeydown(event: KeyboardEvent): void { if (event.defaultPrevented) { return; } if (event.key === "ArrowUp") { event.preventDefault(); changeHeight(1); return; } if (event.key === "ArrowDown") { event.preventDefault(); changeHeight(-1); } } titleEl.textContent = bootstrap.mapName || bootstrap.mapId || "Worldshaper Height Viewer"; metaEl.textContent = bootstrap.mapId + " | " + mapWidth + "x" + mapHeight + " | tile " + tileSize + "px | " + heightLayers.length + " height patch" + (heightLayers.length === 1 ? "" : "es"); const persistBounds = () => { persistWorldshaperHeightViewerBounds(window); }; const persistBoundsDeferred = createDebouncedCallback(() => { persistBounds(); }, 160); heightDownBtn?.addEventListener("click", () => changeHeight(-1)); heightUpBtn?.addEventListener("click", () => changeHeight(1)); window.addEventListener("keydown", handleWindowKeydown); window.addEventListener("resize", () => { invalidateBaseSurface(); draw(); persistBoundsDeferred(); }); viewportEl.addEventListener("scroll", () => { draw(); }, { passive: true }); drawNow(); window.addEventListener("beforeunload", persistBounds); } async function initHeightViewer(): Promise { ensureStyles(); renderLoading("Preparing world snapshot..."); const params = new URLSearchParams(window.location.search); const token = params.get("token")?.trim() || ""; const requestedWorldId = params.get("worldId")?.trim() || params.get("mapId")?.trim() || ""; let bootstrap = loadWorldshaperStudioBootstrap(token); if (!bootstrap) { try { bootstrap = await loadStandaloneWorldshaperBootstrap(requestedWorldId, window.location.origin); } catch (error) { renderError(String(error || "Failed to load the height viewer.")); return; } } if (!bootstrap) { renderError("No world data was available for the height viewer."); return; } startViewer(bootstrap); } void initHeightViewer();