import { events } from "../core/events.js";
import { store } from "../core/store.js";
// ── Helpers ───────────────────────────────────────────────────────────────────
function getDialog(id) {
return /** @type {HTMLDialogElement} */ (document.getElementById(id));
}
function closeOnBackdrop(dialog) {
dialog.addEventListener("click", (e) => {
if (e.target === dialog) dialog.close();
});
}
function wireCloseButtons(dialog) {
dialog.querySelectorAll("[data-close]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close());
});
}
// ── Menu dialog ───────────────────────────────────────────────────────────────
/**
* Show the main menu. Returns a Promise that resolves with the chosen action.
* Possible resolved values: "new-game" | "load-game" | "help" | null (dismissed)
* @returns {Promise<string|null>}
*/
export async function showMenu() {
const hasSave = await store.hasSave();
return new Promise((resolve) => {
const dialog = getDialog("menu-dialog");
// Dim "Load Game" when there's nothing to load
const loadItem = dialog.querySelector('[data-action="load-game"]');
if (loadItem) {
loadItem.style.opacity = hasSave ? "" : "0.4";
loadItem.style.pointerEvents = hasSave ? "" : "none";
}
const onAction = (e) => {
const li = e.target.closest("[data-action]");
if (!li) return;
dialog.close();
dialog.removeEventListener("click", onAction);
resolve(li.dataset.action);
};
dialog.addEventListener("click", onAction);
dialog.addEventListener(
"close",
() => {
dialog.removeEventListener("click", onAction);
resolve(null);
},
{ once: true },
);
dialog.showModal();
});
}
// ── Scenario dialog ───────────────────────────────────────────────────────────
/**
* Show scenario selection list.
* @param {Array<{id:string, start:string, name:string, description:string, creator:string}>} scenarios
* @returns {Promise<{id:string, start:string}|null>}
*/
export function showScenarioDialog(scenarios) {
return new Promise((resolve) => {
const dialog = getDialog("scenario-dialog");
const list = document.getElementById("scenario-list");
list.innerHTML = "";
for (const s of scenarios) {
const li = document.createElement("li");
li.dataset.id = s.id;
li.innerHTML = `
<span class="s-name">${s.name}</span>
<span class="s-meta">by ${s.creator || "unknown"}</span>
<span class="s-desc">${s.description || ""}</span>
`;
li.addEventListener("click", () => {
dialog.close();
resolve(s);
});
list.appendChild(li);
}
wireCloseButtons(dialog);
dialog.addEventListener("close", () => resolve(null), { once: true });
dialog.showModal();
});
}
// ── Save / load dialog ────────────────────────────────────────────────────────
/**
* Show save dialog. The `onSave` callback receives the chosen slot name.
* @param {function(string): void} onSave
*/
export async function showSaveDialog(onSave) {
const existingSaves = await store.listSaves();
const dialog = getDialog("saveload-dialog");
const title = document.getElementById("saveload-title");
const confirmBtn = document.getElementById("saveload-confirm");
const deleteBtn = /** @type {HTMLButtonElement} */ (
document.getElementById("saveload-delete")
);
const nameWrap = document.getElementById("save-name-wrap");
const nameInput = /** @type {HTMLInputElement} */ (
document.getElementById("save-name-input")
);
const slotList = document.getElementById("slot-list");
if (title) title.textContent = "Save Game";
if (nameWrap) nameWrap.style.display = "";
if (confirmBtn) confirmBtn.textContent = "Save";
if (nameInput) nameInput.value = "";
if (deleteBtn) deleteBtn.disabled = true;
let selectedSlotName = null;
// Populate existing saves so the player can click one to overwrite
slotList.innerHTML = "";
for (const save of existingSaves) {
const li = document.createElement("li");
const date = new Date(save.savedAt).toLocaleString();
li.innerHTML = `<span>${save.slotName}</span><span style="opacity:0.6;font-size:0.75em">${date}</span>`;
li.addEventListener("click", () => {
slotList
.querySelectorAll("li")
.forEach((el) => el.classList.remove("selected"));
li.classList.add("selected");
if (nameInput) nameInput.value = save.slotName;
selectedSlotName = save.slotName;
if (deleteBtn) deleteBtn.disabled = false;
});
slotList.appendChild(li);
}
wireCloseButtons(dialog);
const deleteHandler = async () => {
if (!selectedSlotName) return;
await store.delete(selectedSlotName);
selectedSlotName = null;
if (deleteBtn) deleteBtn.disabled = true;
if (nameInput) nameInput.value = "";
// Remove the deleted entry from the list
slotList.querySelectorAll("li.selected").forEach((el) => el.remove());
};
if (deleteBtn) deleteBtn.addEventListener("click", deleteHandler);
const handler = () => {
const name = nameInput?.value.trim() || "My Save";
dialog.close();
onSave(name);
};
confirmBtn.addEventListener("click", handler, { once: true });
dialog.addEventListener(
"close",
() => {
confirmBtn.removeEventListener("click", handler);
if (deleteBtn) deleteBtn.removeEventListener("click", deleteHandler);
},
{ once: true },
);
dialog.showModal();
nameInput?.focus();
}
/**
* Show load dialog with a list of saves to choose from.
* The `onLoad` callback receives the chosen slot name.
* @param {function(string): void} onLoad
*/
export async function showLoadDialog() {
const saves = await store.listSaves();
if (saves.length === 0) {
showConfirm("No saved games found.", () => {}, { okOnly: true });
return null;
}
const dialog = getDialog("saveload-dialog");
const title = document.getElementById("saveload-title");
const confirmBtn = document.getElementById("saveload-confirm");
const deleteBtn = /** @type {HTMLButtonElement} */ (
document.getElementById("saveload-delete")
);
const nameWrap = document.getElementById("save-name-wrap");
const slotList = document.getElementById("slot-list");
if (title) title.textContent = "Load Game";
if (nameWrap) nameWrap.style.display = "none";
if (confirmBtn) {
confirmBtn.textContent = "Load";
confirmBtn.disabled = true;
}
if (deleteBtn) deleteBtn.disabled = true;
let selectedSlot = null;
let _resolve;
const resolveWith = (value) => {
if (_resolve) {
const r = _resolve;
_resolve = null;
r(value);
}
};
function renderList(saveList) {
slotList.innerHTML = "";
for (const save of saveList) {
const li = document.createElement("li");
const date = new Date(save.savedAt).toLocaleString();
li.innerHTML = `<span>${save.slotName}</span><span style="opacity:0.6;font-size:0.75em">${date}</span>`;
if (save.slotName === selectedSlot) li.classList.add("selected");
li.addEventListener("click", () => {
slotList
.querySelectorAll("li")
.forEach((el) => el.classList.remove("selected"));
li.classList.add("selected");
selectedSlot = save.slotName;
if (deleteBtn) deleteBtn.disabled = false;
if (confirmBtn) confirmBtn.disabled = false;
});
li.addEventListener("dblclick", () => {
resolveWith(save.slotName);
dialog.close();
});
slotList.appendChild(li);
}
}
// No pre-selection — buttons stay disabled until user clicks a row
renderList(saves);
wireCloseButtons(dialog);
const deleteHandler = async () => {
if (!selectedSlot) return;
await store.delete(selectedSlot);
const remaining = await store.listSaves();
if (remaining.length === 0) {
dialog.close();
return;
}
selectedSlot = remaining[0].slotName;
if (deleteBtn) deleteBtn.disabled = true;
renderList(remaining);
};
if (deleteBtn) deleteBtn.addEventListener("click", deleteHandler);
const handler = () => {
if (selectedSlot) {
resolveWith(selectedSlot);
dialog.close();
}
};
confirmBtn.addEventListener("click", handler, { once: true });
return new Promise((resolve) => {
_resolve = resolve;
dialog.addEventListener(
"close",
() => {
confirmBtn.removeEventListener("click", handler);
if (deleteBtn) deleteBtn.removeEventListener("click", deleteHandler);
resolveWith(null);
},
{ once: true },
);
dialog.showModal();
});
}
// ── Confirm dialog ────────────────────────────────────────────────────────────
/**
* Show a yes/no confirmation dialog.
* @param {string} message
* @param {function(): void} onConfirm
* @param {{okOnly: boolean}} [opts]
*/
export function showConfirm(message, onConfirm, opts = {}) {
const dialog = getDialog("confirm-dialog");
const msgEl = document.getElementById("confirm-message");
const yesBtn = document.getElementById("confirm-yes");
const noBtn = document.getElementById("confirm-no");
if (msgEl) msgEl.innerHTML = message;
if (noBtn) noBtn.style.display = opts.okOnly ? "none" : "";
if (yesBtn) yesBtn.textContent = opts.okOnly ? "OK" : "Yes";
const onYes = () => {
dialog.close();
onConfirm();
};
const onCancel = opts.preventCancel ? (e) => e.preventDefault() : null;
if (onCancel) dialog.addEventListener("cancel", onCancel);
yesBtn.addEventListener("click", onYes, { once: true });
noBtn.addEventListener("click", () => dialog.close(), { once: true });
dialog.addEventListener(
"close",
() => {
yesBtn.removeEventListener("click", onYes);
if (onCancel) dialog.removeEventListener("cancel", onCancel);
},
{ once: true },
);
dialog.showModal();
}
// ── Win dialog ────────────────────────────────────────────────────────────────
/**
* Show the win screen with an iframe pointing to the win URL.
* @param {string} url relative or absolute URL for the win HTML page
* @param {function(): void} [onClose]
*/
export function showWinDialog(url, onClose) {
const dialog = getDialog("win-dialog");
const frame = document.getElementById("win-frame");
if (frame) frame.src = url;
wireCloseButtons(dialog);
if (onClose) {
dialog.addEventListener("close", onClose, { once: true });
}
dialog.showModal();
}
// ── Modal message ─────────────────────────────────────────────────────────────
/**
* Show a short modal message (used for win conditions, key events, etc.).
* If the message looks like a URL (.html extension), shows the win dialog.
* Otherwise shows a confirm dialog with "OK" only.
* @param {string} message
* @param {function(): void} [onClose]
*/
export function showModalMessage(message, onClose) {
if (message && message.endsWith(".html")) {
showWinDialog(message, onClose);
} else {
showConfirm(message, () => {}, { okOnly: true });
}
}
// ── Help dialog ───────────────────────────────────────────────────────────────
export function showHelp() {
const dialog = getDialog("help-dialog");
wireCloseButtons(dialog);
closeOnBackdrop(dialog);
return new Promise((resolve) => {
dialog.addEventListener("close", () => resolve(), { once: true });
dialog.showModal();
});
}
// ── Wiring ────────────────────────────────────────────────────────────────────
/**
* Connect the events bus to the modal-message dialog.
* Call once during app init.
* @param {function(): void} [onWinClose] called when win dialog is closed
*/
export function initDialogs(onWinClose) {
events.onModalMessage((msg) => {
// Win conditions still use the full dialog
if (msg && msg.endsWith(".html")) {
showWinDialog(msg, onWinClose);
}
// All other modal messages are handled as positional popups in popup-messages.js
});
// Close dialogs on ESC is handled natively by <dialog>
}