(function () { "use strict"; // ── C++ / F4SE bridge placeholders ───────────────────────────── // Real integration will replace these stubs from the plugin side. window.CommonwealthOnlineUI = { openGameSettings: function () { console.log("[CO UI] Request open vanilla Fallout 4 SettingsMenu"); }, setMultiplayerSetting: function (key, value) { console.log("[CO UI] Multiplayer setting changed:", key, value); }, onBack: function () { console.log("[CO UI] Back requested from Settings"); }, onOpenView: function (viewName) { console.log("[CO UI] Open view:", viewName); } }; // ── Compute UI scale ─────────────────────────────────────────── 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 ──────────────── 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) { 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(); // ── DOM refs ─────────────────────────────────────────────────── var pauseScreen = document.getElementById("pauseScreen"); var menu = document.getElementById("menu"); var settingsPanel = document.getElementById("settingsPanel"); var pausePromptBar = document.getElementById("pausePromptBar"); var settingsPromptBar = document.getElementById("settingsPromptBar"); var mapPromptBar = document.getElementById("mapPromptBar"); var mapBackButton = document.getElementById("mapBackButton"); var serverPanel = document.getElementById("serverPanel"); var leftPanel = document.querySelector(".left-panel"); var mapWorld = document.getElementById("mapWorld"); if (!menu || !settingsPanel || !window.CO_Settings) { return; } var settingsController = null; var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item")); var SETTINGS_INDEX = 4; // viewMode: "pause" | "map" | "settings" var viewMode = "pause"; var selectedIndex = -1; var SETTINGS_TRANSITION_MS = 220; var settingsHideFallbackId = 0; var settingsHideOnTransitionEnd = null; // ── Pause menu 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 moveSelection(delta) { var next = selectedIndex + delta; if (selectedIndex < 0) { next = delta > 0 ? 0 : items.length - 1; } else if (next < 0) { next = items.length - 1; } else if (next >= items.length) { next = 0; } setSelected(next); } 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)"); window.CommonwealthOnlineUI.onOpenView("resume"); setSelected(-1); } else if (action === "map") { enterMapMode(); } else if (action === "settings") { enterSettingsMode(); } else if (action === "social") { window.CommonwealthOnlineUI.onOpenView("social"); } else if (action === "server") { window.CommonwealthOnlineUI.onOpenView("server"); } else if (action === "help") { window.CommonwealthOnlineUI.onOpenView("help"); } else if (action === "quit") { console.log("[CO Pause Menu] Quit to main menu (concept)"); window.CommonwealthOnlineUI.onOpenView("quit"); setSelected(-1); } } items.forEach(function (item, index) { item.addEventListener("click", function () { if (viewMode === "settings") { if (index === SETTINGS_INDEX) { return; } exitSettingsMode(); } if (viewMode === "map") { return; } setSelected(index); activateSelected(); }); }); // ── View mode transitions ────────────────────────────────────── function cancelSettingsPanelHide() { if (settingsHideFallbackId) { window.clearTimeout(settingsHideFallbackId); settingsHideFallbackId = 0; } if (settingsHideOnTransitionEnd) { settingsPanel.removeEventListener("transitionend", settingsHideOnTransitionEnd); settingsHideOnTransitionEnd = null; } } function showSettingsPanelAnimated() { cancelSettingsPanelHide(); if (pauseScreen) { pauseScreen.classList.add("is-settings-mode"); } settingsPanel.hidden = false; settingsPanel.classList.remove("is-open"); void settingsPanel.offsetWidth; settingsPanel.classList.add("is-open"); } function hideSettingsPanelAnimated(done) { if (settingsPanel.hidden) { if (done) { done(); } return; } cancelSettingsPanelHide(); settingsPanel.classList.remove("is-open"); var completed = false; function finishHide() { if (completed) { return; } completed = true; cancelSettingsPanelHide(); if (viewMode === "settings" || settingsPanel.classList.contains("is-open")) { if (done) { done(); } return; } if (pauseScreen) { pauseScreen.classList.remove("is-settings-mode"); } settingsPanel.hidden = true; if (done) { done(); } } settingsHideOnTransitionEnd = function (event) { if (event.target !== settingsPanel || event.propertyName !== "opacity") { return; } finishHide(); }; settingsPanel.addEventListener("transitionend", settingsHideOnTransitionEnd); settingsHideFallbackId = window.setTimeout(finishHide, SETTINGS_TRANSITION_MS + 80); } function enterSettingsMode() { if (viewMode === "map") { exitMapMode(); } viewMode = "settings"; console.log("[CO Pause Menu] Entering settings mode"); window.CommonwealthOnlineUI.onOpenView("settings"); setSelected(SETTINGS_INDEX); showSettingsPanelAnimated(); if (pausePromptBar) { pausePromptBar.hidden = true; } if (mapPromptBar) { mapPromptBar.hidden = true; } if (mapWorld) { mapWorld.classList.add("is-blurred"); mapWorld.classList.remove("is-interactive"); } settingsController.show(); } function exitSettingsMode() { if (viewMode !== "settings") { return; } viewMode = "pause"; console.log("[CO Pause Menu] Exiting settings mode"); window.CommonwealthOnlineUI.onBack(); settingsController.hide(); hideSettingsPanelAnimated(function () { if (pausePromptBar) { pausePromptBar.hidden = false; } setSelected(SETTINGS_INDEX); }); } settingsController = window.CO_Settings.create({ mount: settingsPanel, promptBar: settingsPromptBar, bridge: window.CommonwealthOnlineUI, layout: "embedded", onBack: function () { exitSettingsMode(); } }); function enterMapMode() { if (viewMode === "settings") { cancelSettingsPanelHide(); settingsController.hide(); settingsPanel.classList.remove("is-open"); settingsPanel.hidden = true; if (pauseScreen) { pauseScreen.classList.remove("is-settings-mode"); } } viewMode = "map"; console.log("[CO Pause Menu] Entering map mode"); window.CommonwealthOnlineUI.onOpenView("map"); 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 (viewMode !== "map") { return; } viewMode = "pause"; console.log("[CO Pause Menu] Exiting map mode"); window.CommonwealthOnlineUI.onBack(); if (mapBackButton) { mapBackButton.classList.remove("is-visible"); mapBackButton.setAttribute("aria-hidden", "true"); } if (leftPanel) { leftPanel.classList.remove("is-hidden"); } if (serverPanel) { serverPanel.style.display = ""; } if (mapWorld) { mapWorld.classList.add("is-blurred"); mapWorld.classList.remove("is-interactive"); } if (pausePromptBar) { pausePromptBar.hidden = false; } if (mapPromptBar) { mapPromptBar.hidden = true; } setSelected(-1); } // ── Keyboard navigation ──────────────────────────────────────── function handlePauseKeydown(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(); console.log("[CO Pause Menu] Back"); window.CommonwealthOnlineUI.onBack(); break; default: break; } } function handleMapKeydown(event) { if (event.key === "Escape") { event.preventDefault(); exitMapMode(); } } document.addEventListener("keydown", function (event) { if (viewMode === "settings") { settingsController.handleKeydown(event); return; } if (viewMode === "map") { handleMapKeydown(event); return; } handlePauseKeydown(event); }); // ── Map mode PC back button ──────────────────────────────────── if (mapBackButton) { mapBackButton.addEventListener("click", function (event) { event.stopPropagation(); exitMapMode(); }); } // ── Blur map initially ───────────────────────────────────────── if (mapWorld) { mapWorld.classList.add("is-blurred"); } // ── Map pan/zoom ─────────────────────────────────────────────── if (pauseScreen && mapWorld) { var MAX_ZOOM = 4; var ZOOM_STEP = 0.18; var ZOOM_SMOOTHING = 0.18; var ZOOM_SNAP_EPSILON = 0.001; var PAN_SMOOTHING = 0.28; var PAN_SNAP_EPSILON = 0.15; var PAN_INERTIA_FRICTION = 0.92; var PAN_INERTIA_MIN_SPEED = 0.35; var PAN_VELOCITY_IDLE_MS = 80; var zoom = 1; var targetZoom = 1; var MIN_ZOOM = 1; var panX = 0; var panY = 0; var targetPanX = 0; var targetPanY = 0; var velocityX = 0; var velocityY = 0; var isDragging = false; var lastX = 0; var lastY = 0; var lastMoveTime = 0; var zoomRafId = 0; var panRafId = 0; var zoomFocusScreenX = 0; var zoomFocusScreenY = 0; var zoomFocusWorldX = 0; var zoomFocusWorldY = 0; 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() { var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; var mapAspect = mapNaturalWidth / mapNaturalHeight; var viewportAspect = viewportWidth / viewportHeight; if (mapAspect > viewportAspect) { mapDisplayWidth = viewportWidth; mapDisplayHeight = viewportWidth / mapAspect; } else { mapDisplayHeight = viewportHeight; mapDisplayWidth = viewportHeight * mapAspect; } MIN_ZOOM = Math.max( viewportWidth / mapDisplayWidth, viewportHeight / mapDisplayHeight ); if (zoom < MIN_ZOOM) { zoom = MIN_ZOOM; targetZoom = 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 getPanLimits() { var zoomedMapWidth = mapDisplayWidth * zoom; var zoomedMapHeight = mapDisplayHeight * zoom; var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; return { maxX: Math.max(0, (zoomedMapWidth - viewportWidth) / 2), maxY: Math.max(0, (zoomedMapHeight - viewportHeight) / 2), }; } function clampPanValue(x, y) { var limits = getPanLimits(); return { x: Math.max(-limits.maxX, Math.min(limits.maxX, x)), y: Math.max(-limits.maxY, Math.min(limits.maxY, y)), }; } function clampPan() { var clamped = clampPanValue(panX, panY); panX = clamped.x; panY = clamped.y; } function clampTargetPan() { var clamped = clampPanValue(targetPanX, targetPanY); targetPanX = clamped.x; targetPanY = clamped.y; } function stopZoomAnimation() { if (zoomRafId !== 0) { cancelAnimationFrame(zoomRafId); zoomRafId = 0; } targetZoom = zoom; } function stopPanAnimation() { if (panRafId !== 0) { cancelAnimationFrame(panRafId); panRafId = 0; } targetPanX = panX; targetPanY = panY; velocityX = 0; velocityY = 0; } function applyZoomFocusPan() { // Keep the focused map point under the cursor while scale changes // (transform-origin is center; pan is applied after scale). panX = zoomFocusScreenX - zoomFocusWorldX * zoom; panY = zoomFocusScreenY - zoomFocusWorldY * zoom; clampPan(); targetPanX = panX; targetPanY = panY; } function tickZoomAnimation() { zoomRafId = 0; var zoomDelta = targetZoom - zoom; if (Math.abs(zoomDelta) <= ZOOM_SNAP_EPSILON) { zoom = targetZoom; } else { zoom += zoomDelta * ZOOM_SMOOTHING; } applyZoomFocusPan(); applyMapTransform(); if (zoom !== targetZoom) { zoomRafId = requestAnimationFrame(tickZoomAnimation); } } function startZoomAnimation() { stopPanAnimation(); if (zoomRafId === 0) { zoomRafId = requestAnimationFrame(tickZoomAnimation); } } function tickPanAnimation() { panRafId = 0; var keepGoing = false; if (isDragging) { var dragDx = targetPanX - panX; var dragDy = targetPanY - panY; if (Math.abs(dragDx) <= PAN_SNAP_EPSILON && Math.abs(dragDy) <= PAN_SNAP_EPSILON) { panX = targetPanX; panY = targetPanY; } else { panX += dragDx * PAN_SMOOTHING; panY += dragDy * PAN_SMOOTHING; keepGoing = true; } clampPan(); } else { var nextX = panX + velocityX; var nextY = panY + velocityY; var clamped = clampPanValue(nextX, nextY); panX = clamped.x; panY = clamped.y; targetPanX = panX; targetPanY = panY; if (panX !== nextX) { velocityX = 0; } if (panY !== nextY) { velocityY = 0; } velocityX *= PAN_INERTIA_FRICTION; velocityY *= PAN_INERTIA_FRICTION; if ( Math.abs(velocityX) > PAN_INERTIA_MIN_SPEED || Math.abs(velocityY) > PAN_INERTIA_MIN_SPEED ) { keepGoing = true; } else { velocityX = 0; velocityY = 0; } } applyMapTransform(); if (keepGoing) { panRafId = requestAnimationFrame(tickPanAnimation); } } function startPanAnimation() { if (panRafId === 0) { panRafId = requestAnimationFrame(tickPanAnimation); } } mapWorld.addEventListener( "wheel", function (event) { if (viewMode !== "map") { return; } event.preventDefault(); var nextTarget = Math.max( MIN_ZOOM, Math.min(MAX_ZOOM, targetZoom + (event.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP)) ); if (nextTarget === targetZoom && zoom === targetZoom) { return; } // Anchor on the map point currently under the cursor. zoomFocusScreenX = event.clientX - window.innerWidth / 2; zoomFocusScreenY = event.clientY - window.innerHeight / 2; zoomFocusWorldX = (zoomFocusScreenX - panX) / zoom; zoomFocusWorldY = (zoomFocusScreenY - panY) / zoom; targetZoom = nextTarget; startZoomAnimation(); }, { passive: false } ); mapWorld.addEventListener("mousedown", function (event) { if (viewMode !== "map" || zoom <= MIN_ZOOM) { return; } stopZoomAnimation(); isDragging = true; targetPanX = panX; targetPanY = panY; velocityX = 0; velocityY = 0; lastX = event.clientX; lastY = event.clientY; lastMoveTime = performance.now(); pauseScreen.classList.add("is-panning"); startPanAnimation(); }); window.addEventListener("mousemove", function (event) { if (!isDragging) { return; } var dx = event.clientX - lastX; var dy = event.clientY - lastY; var now = performance.now(); targetPanX += dx; targetPanY += dy; clampTargetPan(); // Blend recent movement into release velocity for inertia. velocityX = velocityX * 0.55 + dx * 0.45; velocityY = velocityY * 0.55 + dy * 0.45; lastX = event.clientX; lastY = event.clientY; lastMoveTime = now; startPanAnimation(); }); window.addEventListener("mouseup", function () { if (!isDragging) { return; } isDragging = false; pauseScreen.classList.remove("is-panning"); // If the pointer stopped before release, do not coast. if (performance.now() - lastMoveTime > PAN_VELOCITY_IDLE_MS) { velocityX = 0; velocityY = 0; } // Fold any remaining ease-catchup into the coast. velocityX += (targetPanX - panX) * 0.2; velocityY += (targetPanY - panY) * 0.2; targetPanX = panX; targetPanY = panY; startPanAnimation(); }); } })();