Files
Commonwealth-Online-Public/ui/views/CommonwealthOnline/main-menu/script.js
T
andrew 2a3e614a47 Add quit confirmation to main menu
Implement a quit confirmation dialog that appears when the QUIT button is selected on the main menu. The dialog displays 'EXIT GAME?' with YES/NO options. YES sends a coAction quit request to the plugin, which hides the overlay, disconnects any active server session, and sets RE::Main::quitGame to exit the game. NO returns to the main menu. Includes full keyboard (arrow keys, Enter, Escape), gamepad (dpad, A/B buttons), and mouse support. Default selection is NO.
2026-07-07 22:33:33 +12:00

835 lines
22 KiB
JavaScript

(function () {
"use strict";
// ── C++ / F4SE bridge placeholders ─────────────────────────────
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 main menu");
},
onOpenView: function (viewName) {
console.log("[CO UI] Open view:", viewName);
}
};
// ── Pointer mode API for C++ invocation (patched after browser mount) ──
window.CO_App = {};
function sendCoAction(payload) {
var msg = JSON.stringify(payload);
if (typeof window.coAction === "function") {
window.coAction(msg);
return true;
}
console.warn("[CO Main Menu] coAction bridge unavailable:", payload);
return false;
}
// ── 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 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 mainScreen = document.getElementById("mainScreen");
var menu = document.getElementById("menu");
var settingsPanel = document.getElementById("settingsPanel");
var browserPanel = document.getElementById("browserPanel");
var quitConfirmPanel = document.getElementById("quitConfirmPanel");
var quitConfirmOptions = document.getElementById("quitConfirmOptions");
var mainPromptBar = document.getElementById("mainPromptBar");
var settingsPromptBar = document.getElementById("settingsPromptBar");
if (!menu || !settingsPanel || !browserPanel || !quitConfirmPanel || !quitConfirmOptions ||
!window.CO_Settings || !window.CO_Browser) {
return;
}
var settingsController = null;
var browserController = null;
var items = Array.prototype.slice.call(menu.querySelectorAll(".menu-item"));
var quitConfirmItems = Array.prototype.slice.call(
quitConfirmOptions.querySelectorAll(".quit-confirm__option")
);
var MULTIPLAYER_INDEX = 0;
var SETTINGS_INDEX = 2;
var QUIT_INDEX = 4;
// viewMode: "menu" | "settings" | "browser" | "quitConfirm"
var viewMode = "menu";
var selectedIndex = -1;
var quitConfirmSelectedIndex = 0;
var PANEL_TRANSITION_MS = 220;
var settingsHideFallbackId = 0;
var settingsHideOnTransitionEnd = null;
var browserHideFallbackId = 0;
var browserHideOnTransitionEnd = null;
// ── Main 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;
}
// Skip disabled items
var guard = 0;
while (items[next] && items[next].classList.contains("is-disabled") && guard < items.length) {
next += delta > 0 ? 1 : -1;
if (next < 0) {
next = items.length - 1;
} else if (next >= items.length) {
next = 0;
}
guard += 1;
}
setSelected(next);
}
function activateSelected() {
var item = items[selectedIndex];
if (!item || item.classList.contains("is-disabled")) {
return;
}
var action = item.getAttribute("data-action");
console.log("[CO Main Menu] Selected:", action);
if (action === "settings") {
enterSettingsMode();
} else if (action === "multiplayer") {
enterBrowserMode();
} else if (action === "quit") {
enterQuitConfirmMode();
} else {
window.CommonwealthOnlineUI.onOpenView(action);
setSelected(-1);
}
}
items.forEach(function (item, index) {
item.addEventListener("click", function () {
if (viewMode === "settings") {
if (index === SETTINGS_INDEX) {
return;
}
exitSettingsMode();
} else if (viewMode === "browser") {
if (index === MULTIPLAYER_INDEX) {
return;
}
exitBrowserMode();
} else if (viewMode === "quitConfirm") {
if (index === QUIT_INDEX) {
return;
}
exitQuitConfirmMode();
}
if (item.classList.contains("is-disabled")) {
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 (mainScreen) {
mainScreen.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 (mainScreen) {
mainScreen.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, PANEL_TRANSITION_MS + 80);
}
function cancelBrowserPanelHide() {
if (browserHideFallbackId) {
window.clearTimeout(browserHideFallbackId);
browserHideFallbackId = 0;
}
if (browserHideOnTransitionEnd) {
browserPanel.removeEventListener("transitionend", browserHideOnTransitionEnd);
browserHideOnTransitionEnd = null;
}
}
function showBrowserPanelAnimated() {
cancelBrowserPanelHide();
if (mainScreen) {
mainScreen.classList.add("is-browser-mode");
}
browserPanel.hidden = false;
browserPanel.classList.remove("is-open");
void browserPanel.offsetWidth;
browserPanel.classList.add("is-open");
}
function hideBrowserPanelAnimated(done) {
if (browserPanel.hidden) {
if (done) {
done();
}
return;
}
cancelBrowserPanelHide();
browserPanel.classList.remove("is-open");
var completed = false;
function finishHide() {
if (completed) {
return;
}
completed = true;
cancelBrowserPanelHide();
if (viewMode === "browser" || browserPanel.classList.contains("is-open")) {
if (done) {
done();
}
return;
}
if (mainScreen) {
mainScreen.classList.remove("is-browser-mode");
}
browserPanel.hidden = true;
if (done) {
done();
}
}
browserHideOnTransitionEnd = function (event) {
if (event.target !== browserPanel || event.propertyName !== "opacity") {
return;
}
finishHide();
};
browserPanel.addEventListener("transitionend", browserHideOnTransitionEnd);
browserHideFallbackId = window.setTimeout(finishHide, PANEL_TRANSITION_MS + 80);
}
function enterBrowserMode(fromNative) {
if (viewMode === "browser") {
if (fromNative) {
browserController.onShown();
}
return;
}
if (viewMode === "settings") {
exitSettingsMode();
}
viewMode = "browser";
console.log("[CO Main Menu] Entering browser mode");
window.CommonwealthOnlineUI.onOpenView("multiplayer");
setSelected(MULTIPLAYER_INDEX);
showBrowserPanelAnimated();
if (mainPromptBar) {
mainPromptBar.hidden = true;
}
browserController.show();
browserController.onShown();
if (!fromNative) {
sendCoAction({ action: "openView", view: "multiplayer" });
}
}
function exitBrowserMode(fromNative) {
if (viewMode !== "browser") {
return;
}
viewMode = "menu";
console.log("[CO Main Menu] Exiting browser mode");
window.CommonwealthOnlineUI.onBack();
browserController.hide();
hideBrowserPanelAnimated(function () {
if (mainPromptBar) {
mainPromptBar.hidden = false;
}
if (viewMode === "menu") {
setSelected(MULTIPLAYER_INDEX);
}
});
if (!fromNative) {
sendCoAction({ action: "closeBrowser" });
}
}
window.onBrowserShown = function () {
enterBrowserMode(true);
};
window.closeBrowser = function () {
exitBrowserMode(true);
};
function setQuitConfirmSelected(index) {
if (index >= quitConfirmItems.length) {
return;
}
quitConfirmSelectedIndex = index < 0 ? 0 : index;
quitConfirmItems.forEach(function (item, i) {
var isSelected = i === quitConfirmSelectedIndex;
item.classList.toggle("is-selected", isSelected);
item.setAttribute("aria-selected", isSelected ? "true" : "false");
item.tabIndex = isSelected ? 0 : -1;
});
}
function moveQuitConfirmSelection(delta) {
var next = quitConfirmSelectedIndex + delta;
if (next < 0) {
next = quitConfirmItems.length - 1;
} else if (next >= quitConfirmItems.length) {
next = 0;
}
setQuitConfirmSelected(next);
}
function showQuitConfirmPanelAnimated() {
if (mainScreen) {
mainScreen.classList.add("is-quit-confirm-mode");
}
quitConfirmPanel.hidden = false;
quitConfirmPanel.classList.remove("is-open");
void quitConfirmPanel.offsetWidth;
quitConfirmPanel.classList.add("is-open");
}
function hideQuitConfirmPanelAnimated(done) {
if (quitConfirmPanel.hidden) {
if (done) {
done();
}
return;
}
quitConfirmPanel.classList.remove("is-open");
window.setTimeout(function finishHide() {
if (viewMode === "quitConfirm" || quitConfirmPanel.classList.contains("is-open")) {
if (done) {
done();
}
return;
}
if (mainScreen) {
mainScreen.classList.remove("is-quit-confirm-mode");
}
quitConfirmPanel.hidden = true;
if (done) {
done();
}
}, PANEL_TRANSITION_MS + 80);
}
function enterQuitConfirmMode() {
if (viewMode === "quitConfirm") {
return;
}
if (viewMode === "settings") {
exitSettingsMode();
} else if (viewMode === "browser") {
exitBrowserMode();
}
viewMode = "quitConfirm";
console.log("[CO Main Menu] Entering quit confirmation");
setSelected(QUIT_INDEX);
setQuitConfirmSelected(1);
showQuitConfirmPanelAnimated();
}
function exitQuitConfirmMode() {
if (viewMode !== "quitConfirm") {
return;
}
viewMode = "menu";
console.log("[CO Main Menu] Exiting quit confirmation");
hideQuitConfirmPanelAnimated(function () {
if (viewMode === "menu") {
setSelected(QUIT_INDEX);
}
});
}
function confirmQuit() {
console.log("[CO Main Menu] Quit confirmed");
sendCoAction({ action: "quit" });
}
function activateQuitConfirmSelected() {
var item = quitConfirmItems[quitConfirmSelectedIndex];
if (!item) {
return;
}
var choice = item.getAttribute("data-choice");
if (choice === "yes") {
confirmQuit();
return;
}
exitQuitConfirmMode();
}
quitConfirmItems.forEach(function (item, index) {
item.addEventListener("click", function () {
setQuitConfirmSelected(index);
activateQuitConfirmSelected();
});
});
window.onMainMenuShown = function () {
if (mainScreen) {
mainScreen.classList.remove("is-session-loading");
}
};
window.onSessionLoading = function () {
if (viewMode === "browser") {
cancelBrowserPanelHide();
browserController.hide();
browserPanel.classList.remove("is-open");
browserPanel.hidden = true;
} else if (viewMode === "settings") {
cancelSettingsPanelHide();
settingsController.hide();
settingsPanel.classList.remove("is-open");
settingsPanel.hidden = true;
} else if (viewMode === "quitConfirm") {
quitConfirmPanel.classList.remove("is-open");
quitConfirmPanel.hidden = true;
}
viewMode = "menu";
if (mainScreen) {
mainScreen.classList.remove("is-browser-mode", "is-settings-mode", "is-quit-confirm-mode");
mainScreen.classList.add("is-session-loading");
}
if (mainPromptBar) {
mainPromptBar.hidden = true;
}
if (settingsPromptBar) {
settingsPromptBar.hidden = true;
}
setSelected(-1);
};
function enterSettingsMode() {
if (viewMode === "browser") {
cancelBrowserPanelHide();
browserController.hide();
browserPanel.classList.remove("is-open");
browserPanel.hidden = true;
if (mainScreen) {
mainScreen.classList.remove("is-browser-mode");
}
}
viewMode = "settings";
console.log("[CO Main Menu] Entering settings mode");
window.CommonwealthOnlineUI.onOpenView("settings");
setSelected(SETTINGS_INDEX);
showSettingsPanelAnimated();
if (mainPromptBar) {
mainPromptBar.hidden = true;
}
settingsController.show();
}
function exitSettingsMode() {
if (viewMode !== "settings") {
return;
}
viewMode = "menu";
console.log("[CO Main Menu] Exiting settings mode");
window.CommonwealthOnlineUI.onBack();
settingsController.hide();
hideSettingsPanelAnimated(function () {
if (mainPromptBar) {
mainPromptBar.hidden = false;
}
if (viewMode === "menu") {
setSelected(SETTINGS_INDEX);
}
});
}
settingsController = window.CO_Settings.create({
mount: settingsPanel,
promptBar: settingsPromptBar,
bridge: window.CommonwealthOnlineUI,
layout: "embedded",
onBack: function () {
exitSettingsMode();
}
});
browserController = window.CO_Browser.create({
mount: browserPanel,
layout: "embedded",
exposeGlobals: true,
captureKeys: false,
onBack: function () {
exitBrowserMode();
}
});
window.CO_App.setPointerMode = function (mode) {
if (viewMode === "browser") {
browserController.setPointerMode(mode);
}
};
// ── Keyboard navigation ────────────────────────────────────────
function handleQuitConfirmKeydown(event) {
switch (event.key) {
case "ArrowUp":
event.preventDefault();
moveQuitConfirmSelection(-1);
break;
case "ArrowDown":
event.preventDefault();
moveQuitConfirmSelection(1);
break;
case "Enter":
case " ":
event.preventDefault();
activateQuitConfirmSelected();
break;
case "Escape":
event.preventDefault();
exitQuitConfirmMode();
break;
default:
break;
}
}
function handleMenuKeydown(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 Main Menu] Back");
window.CommonwealthOnlineUI.onBack();
break;
default:
break;
}
}
document.addEventListener("keydown", function (event) {
if (viewMode === "settings") {
settingsController.handleKeydown(event);
return;
}
if (viewMode === "browser") {
browserController.handleKeydown(event);
return;
}
if (viewMode === "quitConfirm") {
handleQuitConfirmKeydown(event);
return;
}
handleMenuKeydown(event);
});
window.handleControllerInput = function (button, phase) {
if (phase === "down") {
if (viewMode === "browser") {
browserController.handleControllerInput(button, phase);
return;
}
if (viewMode === "settings") {
return;
}
if (viewMode === "quitConfirm") {
switch (button) {
case "dpad_up":
moveQuitConfirmSelection(-1);
break;
case "dpad_down":
moveQuitConfirmSelection(1);
break;
case "a":
activateQuitConfirmSelected();
break;
case "b":
exitQuitConfirmMode();
break;
}
return;
}
switch (button) {
case "dpad_up":
moveSelection(-1);
break;
case "dpad_down":
moveSelection(1);
break;
case "a":
activateSelected();
break;
case "b":
if (viewMode === "browser") {
exitBrowserMode();
} else if (viewMode === "settings") {
exitSettingsMode();
}
break;
}
}
};
})();