(function () { "use strict"; // ── Compute UI scale (fixes browser incompatibility with nested calc/min/clamp in custom props) ── // Some browsers don't resolve nested calc()/min()/clamp() inside custom properties to a number, // leaving them as formula strings. That breaks calc(Npx * var(--co-ui-scale)) throughout the sheet. // Instead, we compute the scale in JS and set it as a numeric value, which works everywhere. function updateScale() { var MIN_SCALE = 0.5; var MAX_SCALE = 1.6; var BASELINE_WIDTH = 1920; var BASELINE_HEIGHT = 1080; var scale = Math.min( window.innerWidth / BASELINE_WIDTH, window.innerHeight / BASELINE_HEIGHT ); scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); document.documentElement.style.setProperty("--co-ui-scale", String(scale)); } updateScale(); window.addEventListener("resize", updateScale); // ── Tint Xbox prompt glyphs to pause-menu amber (same approach as browser) ── // White pixels become --co-amber; black stays black; transparent stays transparent. var GLYPH_WHITE_THRESHOLD = 200; var GLYPH_BLACK_THRESHOLD = 40; var glyphTintCache = {}; function getAmberTint() { var style = getComputedStyle(document.documentElement); var amber = style.getPropertyValue("--co-amber").trim() || "#c9a84a"; var hex = amber.replace("#", ""); if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } return { r: parseInt(hex.slice(0, 2), 16) || 201, g: parseInt(hex.slice(2, 4), 16) || 168, b: parseInt(hex.slice(4, 6), 16) || 74 }; } function classifyGlyphPixel(r, g, b) { if (r >= GLYPH_WHITE_THRESHOLD && g >= GLYPH_WHITE_THRESHOLD && b >= GLYPH_WHITE_THRESHOLD) { return "white"; } if (r <= GLYPH_BLACK_THRESHOLD && g <= GLYPH_BLACK_THRESHOLD && b <= GLYPH_BLACK_THRESHOLD) { return "black"; } return "edge"; } function tintGlyphImageData(imageData, color) { var data = imageData.data; for (var i = 0; i < data.length; i += 4) { if (data[i + 3] < 4) { data[i + 3] = 0; continue; } var kind = classifyGlyphPixel(data[i], data[i + 1], data[i + 2]); if (kind === "white") { data[i] = color.r; data[i + 1] = color.g; data[i + 2] = color.b; } else if (kind === "black") { data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; } else { var lum = (data[i] + data[i + 1] + data[i + 2]) / (3 * 255); data[i] = Math.round(lum * color.r); data[i + 1] = Math.round(lum * color.g); data[i + 2] = Math.round(lum * color.b); } } return imageData; } function tintGlyphImage(url, color, done) { var key = url + "|" + color.r + "," + color.g + "," + color.b; if (glyphTintCache[key]) { done(glyphTintCache[key]); return; } var img = new Image(); img.onload = function () { var canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; var ctx = canvas.getContext("2d"); if (!ctx) { done(null); return; } ctx.drawImage(img, 0, 0); try { ctx.putImageData( tintGlyphImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), color), 0, 0 ); } catch (err) { // file:// or cross-origin can taint the canvas; keep the original white glyph. done(null); return; } var dataUrl = canvas.toDataURL("image/png"); glyphTintCache[key] = dataUrl; done(dataUrl); }; img.onerror = function () { done(null); }; img.src = url; } function hydratePromptGlyphs() { var color = getAmberTint(); var imgs = document.querySelectorAll(".prompt__glyph-img[data-glyph-src]"); for (var i = 0; i < imgs.length; i++) { (function (img) { var src = img.getAttribute("data-glyph-src"); if (!src) { return; } tintGlyphImage(src, color, function (dataUrl) { if (dataUrl) { img.src = dataUrl; } }); })(imgs[i]); } } hydratePromptGlyphs(); var menu = document.getElementById("menu"); if (!menu) { return; } var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item")); var selectedIndex = -1; // No default selection function setSelected(index) { if (index >= items.length) { return; } selectedIndex = index < 0 ? -1 : index; items.forEach(function (item, i) { var isSelected = i === selectedIndex; item.classList.toggle("is-selected", isSelected); item.setAttribute("aria-selected", isSelected ? "true" : "false"); item.tabIndex = isSelected ? 0 : -1; }); } function activateSelected() { var item = items[selectedIndex]; if (!item) { return; } var action = item.getAttribute("data-action"); console.log("[CO Pause Menu] Selected:", action); if (action === "resume") { console.log("[CO Pause Menu] Resume game (concept)"); setSelected(-1); } else if (action === "map") { enterMapMode(); } else if (action === "quit") { console.log("[CO Pause Menu] Quit to main menu (concept)"); setSelected(-1); } } function moveSelection(delta) { var next = selectedIndex + delta; if (next < 0) { next = items.length - 1; } else if (next >= items.length) { next = 0; } setSelected(next); } items.forEach(function (item, index) { item.addEventListener("click", function () { setSelected(index); activateSelected(); }); }); document.addEventListener("keydown", function (event) { switch (event.key) { case "ArrowUp": event.preventDefault(); moveSelection(-1); break; case "ArrowDown": event.preventDefault(); moveSelection(1); break; case "Enter": case " ": event.preventDefault(); activateSelected(); break; case "Escape": event.preventDefault(); exitMapMode(); break; default: break; } }); if (selectedIndex >= 0) { setSelected(selectedIndex); } // ── Map mode state management ─────────────────────────────────── var isMapMode = false; function enterMapMode() { isMapMode = true; console.log("[CO Pause Menu] Entering map mode"); var leftPanel = document.querySelector(".left-panel"); var mapWorld = document.getElementById("mapWorld"); var pausePromptBar = document.getElementById("pausePromptBar"); var mapPromptBar = document.getElementById("mapPromptBar"); var mapBackButton = document.getElementById("mapBackButton"); var serverPanel = document.querySelector(".server-panel"); setSelected(-1); if (leftPanel) leftPanel.classList.add("is-hidden"); if (serverPanel) serverPanel.style.display = "none"; if (mapWorld) { mapWorld.classList.remove("is-blurred"); mapWorld.classList.add("is-interactive"); } if (pausePromptBar) pausePromptBar.hidden = true; if (mapPromptBar) mapPromptBar.hidden = false; if (mapBackButton) { mapBackButton.classList.add("is-visible"); mapBackButton.setAttribute("aria-hidden", "false"); } } function exitMapMode() { if (!isMapMode) { return; } isMapMode = false; console.log("[CO Pause Menu] Exiting map mode"); var leftPanel = document.querySelector(".left-panel"); var mapWorld = document.getElementById("mapWorld"); var pausePromptBar = document.getElementById("pausePromptBar"); var mapPromptBar = document.getElementById("mapPromptBar"); var mapBackButton = document.getElementById("mapBackButton"); var serverPanel = document.querySelector(".server-panel"); if (mapBackButton) { mapBackButton.classList.remove("is-visible"); mapBackButton.setAttribute("aria-hidden", "true"); } if (leftPanel) leftPanel.classList.remove("is-hidden"); if (serverPanel) serverPanel.style.display = "block"; if (mapWorld) { mapWorld.classList.add("is-blurred"); mapWorld.classList.remove("is-interactive"); } if (pausePromptBar) pausePromptBar.hidden = false; if (mapPromptBar) mapPromptBar.hidden = true; setSelected(-1); } // ── Blur map initially ──────────────────────────────────────── var initialMapWorld = document.getElementById("mapWorld"); if (initialMapWorld) { initialMapWorld.classList.add("is-blurred"); } // ── Map mode PC back button handler ─────────────────────────── var mapBackButton = document.getElementById("mapBackButton"); if (mapBackButton) { mapBackButton.addEventListener("click", function (event) { event.stopPropagation(); exitMapMode(); }); } // ── Map pan/zoom (concept preview of future Fallout 76-style navigation) ── // Scroll to zoom, click-drag to pan once zoomed in. Real implementation // will eventually read controller right-stick input via F4SE instead. var pauseScreen = document.getElementById("pauseScreen"); var mapWorld = document.getElementById("mapWorld"); if (pauseScreen && mapWorld) { var MAX_ZOOM = 4; var ZOOM_STEP = 0.18; var zoom = 1; var MIN_ZOOM = 1; // Will be recalculated after map loads var panX = 0; var panY = 0; var isDragging = false; var lastX = 0; var lastY = 0; // Load the map image to measure its intrinsic dimensions, so we can // calculate the actual rendered bounds with background-size: contain // and set the dynamic MIN_ZOOM to prevent seeing past edges. var mapImg = new Image(); var mapNaturalWidth = 1024; var mapNaturalHeight = 768; var mapDisplayWidth = 1024; var mapDisplayHeight = 768; mapImg.onload = function () { mapNaturalWidth = mapImg.naturalWidth; mapNaturalHeight = mapImg.naturalHeight; updateMapDisplayBounds(); }; mapImg.src = "assets/map.png"; function updateMapDisplayBounds() { // Given the map's intrinsic size and the viewport size, calculate where // the map actually renders with background-size: contain. var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; var mapAspect = mapNaturalWidth / mapNaturalHeight; var viewportAspect = viewportWidth / viewportHeight; if (mapAspect > viewportAspect) { // Map is wider: fit by width mapDisplayWidth = viewportWidth; mapDisplayHeight = viewportWidth / mapAspect; } else { // Map is taller: fit by height mapDisplayHeight = viewportHeight; mapDisplayWidth = viewportHeight * mapAspect; } // Calculate dynamic MIN_ZOOM so that even at full zoom-out, the map // fills the viewport and you can't see past the edges. // At zoom level Z, the map renders at (mapDisplayWidth * Z) x (mapDisplayHeight * Z). // To fill the viewport: Z >= max(viewportWidth / mapDisplayWidth, viewportHeight / mapDisplayHeight) MIN_ZOOM = Math.max( viewportWidth / mapDisplayWidth, viewportHeight / mapDisplayHeight ); // If already zoomed in below the new MIN_ZOOM (shouldn't happen on init), // snap back to MIN_ZOOM. if (zoom < MIN_ZOOM) { zoom = MIN_ZOOM; panX = 0; panY = 0; applyMapTransform(); } } updateMapDisplayBounds(); window.addEventListener("resize", updateMapDisplayBounds); function applyMapTransform() { pauseScreen.style.setProperty("--map-zoom", String(zoom)); pauseScreen.style.setProperty("--map-pan-x", panX + "px"); pauseScreen.style.setProperty("--map-pan-y", panY + "px"); } function clampPan() { // At the current zoom level, the map renders at (mapDisplayWidth * zoom) x (mapDisplayHeight * zoom). // The viewport is window.innerWidth x window.innerHeight. Pan offsets are clamped so that the // map's edges never go beyond the viewport edges. var zoomedMapWidth = mapDisplayWidth * zoom; var zoomedMapHeight = mapDisplayHeight * zoom; var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; // If the zoomed map is smaller than the viewport, no panning is allowed. var maxPanX = Math.max(0, (zoomedMapWidth - viewportWidth) / 2); var maxPanY = Math.max(0, (zoomedMapHeight - viewportHeight) / 2); panX = Math.max(-maxPanX, Math.min(maxPanX, panX)); panY = Math.max(-maxPanY, Math.min(maxPanY, panY)); } mapWorld.addEventListener( "wheel", function (event) { event.preventDefault(); var delta = event.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP; zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom + delta)); if (zoom === MIN_ZOOM) { panX = 0; panY = 0; } else { clampPan(); } applyMapTransform(); }, { passive: false } ); mapWorld.addEventListener("mousedown", function (event) { if (zoom <= MIN_ZOOM) { return; } isDragging = true; lastX = event.clientX; lastY = event.clientY; pauseScreen.classList.add("is-panning"); }); window.addEventListener("mousemove", function (event) { if (!isDragging) { return; } panX += event.clientX - lastX; panY += event.clientY - lastY; lastX = event.clientX; lastY = event.clientY; clampPan(); applyMapTransform(); }); window.addEventListener("mouseup", function () { isDragging = false; pauseScreen.classList.remove("is-panning"); }); } })();