Add pause menu concept prototype

Adds a standalone Commonwealth Online pause menu mockup with the Fallout-style map, left-side navigation, server info, controller prompts, and keyboard handling. Also includes map zoom/pan behavior, fixes the UI scale custom property so sizes resolve correctly, and updates the changelog and dev log.
This commit is contained in:
2026-07-03 23:04:39 +12:00
parent 153dfe525c
commit db5e50c7fd
9 changed files with 1038 additions and 0 deletions
@@ -0,0 +1,184 @@
(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);
var menu = document.getElementById("menu");
if (!menu) {
return;
}
var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item"));
var selectedIndex = 1; // MAP is default
function setSelected(index) {
if (index < 0 || index >= items.length) {
return;
}
selectedIndex = 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)");
} else if (action === "quit") {
console.log("[CO Pause Menu] Quit to main menu (concept)");
}
}
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();
});
item.addEventListener("mouseenter", function () {
setSelected(index);
});
});
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();
console.log("[CO Pause Menu] Back / close pause menu (concept)");
break;
default:
break;
}
});
setSelected(selectedIndex);
// ── 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 mapLayer = document.querySelector(".map-layer");
if (pauseScreen && mapLayer) {
var MIN_ZOOM = 1;
var MAX_ZOOM = 4;
var ZOOM_STEP = 0.18;
var zoom = MIN_ZOOM;
var panX = 0;
var panY = 0;
var isDragging = false;
var lastX = 0;
var lastY = 0;
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() {
var maxOffsetX = ((zoom - 1) * window.innerWidth) / 2;
var maxOffsetY = ((zoom - 1) * window.innerHeight) / 2;
panX = Math.max(-maxOffsetX, Math.min(maxOffsetX, panX));
panY = Math.max(-maxOffsetY, Math.min(maxOffsetY, panY));
}
mapLayer.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 }
);
mapLayer.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");
});
}
})();