import { events } from "../core/events.js";
import { COLUMNS } from "../core/board.js";
import { game, STATE } from "../core/game.js";
import { HIDES_ITEMS, PLAYER } from "../core/flags.js";
const POPUP_DURATION_MS = 2000;
/**
* Cell popup messages — mirrors the Java "popup menu" feature.
*
* When a non-modal message fires, a small label appears above the relevant
* cell on the board and fades out after 4 seconds.
*/
export function initPopupMessages() {
const boardEl = document.getElementById("board");
const popupLayer = document.getElementById("popup-layer");
if (!boardEl || !popupLayer) return;
events.onMessage((cell, text) => {
if (!text || !cell) return;
_showFadePopup(boardEl, popupLayer, text, cell.x, cell.y);
});
events.onHandleInventoryMessaging(() => {
const cell = game.board?.getCurrentCell();
if (!cell) return;
const text = _buildItemListText(cell);
// Only remove item-list popups, not terrain/trigger message popups
for (const el of popupLayer.querySelectorAll(
".cell-popup[data-item-list]",
)) {
el.remove();
}
if (text) {
_showFadePopup(boardEl, popupLayer, text, cell.x, cell.y, {
isItemList: true,
});
}
});
events.onModalMessage((msg) => {
// Win dialogs (.html) are handled by dialogs.js — skip those here
if (!msg || msg.endsWith(".html")) return;
const cell = game.board?.getCurrentCell();
if (!cell) return;
_showModalPopup(boardEl, popupLayer, msg, cell.x, cell.y);
});
}
/**
* Show a modal (blocking) positional popup. Freezes the game until ESC/Enter.
*/
function _showModalPopup(boardEl, popupLayer, text, cellX, cellY) {
const cellEl = boardEl.children[cellY * COLUMNS + cellX];
if (!cellEl) return;
const cellRect = cellEl.getBoundingClientRect();
const layerRect = popupLayer.getBoundingClientRect();
const popup = document.createElement("div");
popup.className = "cell-popup-modal";
popup.innerHTML = text;
const hint = document.createElement("span");
hint.className = "popup-dismiss-hint";
hint.textContent = "ESC/ENTER to continue…";
popup.appendChild(hint);
popup.style.left = `${cellRect.right - layerRect.left}px`;
popup.style.top = `${cellRect.top - layerRect.top + cellRect.height / 2}px`;
popupLayer.appendChild(popup);
// Freeze the game while the popup is shown
const prevState = game.state;
game.state = STATE.ANIMATING;
function dismiss(e) {
if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
popup.remove();
game.state = prevState;
document.removeEventListener("keydown", dismiss, { capture: true });
}
}
document.addEventListener("keydown", dismiss, { capture: true });
}
/**
* Show a fade-out popup anchored to the right of a cell.
* Returns the popup element so callers can remove it early if needed.
* @param {HTMLElement} boardEl
* @param {HTMLElement} popupLayer
* @param {string} text
* @param {number} cellX
* @param {number} cellY
* @param {{isItemList: boolean}} [options]
* @returns {HTMLElement|null}
*/
function _showFadePopup(
boardEl,
popupLayer,
text,
cellX,
cellY,
{ isItemList = false } = {},
) {
const cellEl = boardEl.children[cellY * COLUMNS + cellX];
if (!cellEl) return null;
const cellRect = cellEl.getBoundingClientRect();
const layerRect = popupLayer.getBoundingClientRect();
const popup = document.createElement("div");
popup.className = "cell-popup";
if (isItemList) popup.dataset.itemList = "true";
popup.innerHTML = text;
popup.style.left = `${cellRect.right - layerRect.left}px`;
popup.style.top = `${cellRect.bottom - layerRect.top + cellRect.height}px`;
popupLayer.appendChild(popup);
setTimeout(() => popup.remove(), POPUP_DURATION_MS);
return popup;
}
/**
* Build the HTML text for the item-list popup, mirroring Java's
* MessageManager.displayItemsAtCurrentCell().
* Returns null if the cell has no items.
* @param {Cell} cell
* @returns {string|null}
*/
function _buildItemListText(cell) {
if (cell.isBagEmpty) return null;
if (cell.terrain?.is(HIDES_ITEMS)) {
return "Some items are hidden here...";
}
// Group items by name (matches Java Bag entry list order)
const groups = [];
const groupMap = new Map();
for (const item of cell.items) {
if (groupMap.has(item.name)) {
groupMap.get(item.name).count++;
} else {
const entry = { item, count: 1 };
groupMap.set(item.name, entry);
groups.push(entry);
}
}
const header =
groups.length > 10
? "Use 'r' to rotate items in list"
: "Use 'p' or # to pick up item";
const len = Math.min(groups.length, 10);
const lines = [header];
for (let i = 0; i < len; i++) {
const j = i < 9 ? i + 1 : 0; // 1–9 then 0 for the 10th
const { item, count } = groups[i];
const sym = item.symbol;
const fg = sym?.color?.hex ?? "inherit";
const bg = sym?.background?.hex ?? "transparent";
const countStr = count > 1 ? ` (x${count})` : "";
lines.push(
`${j}. <span style="color:${fg};background:${bg}">${sym?.entity ?? "?"}</span> ${item.name}${countStr}`,
);
}
return lines.join("<br>");
}
// ── Cell hover info popup ─────────────────────────────────────────────────────
/**
* Show a brief legend popup when the player hovers over a cell.
* Mirrors the Java CellInfoPanel behaviour: 600 ms delay to show,
* auto-hides after 2100 ms or on mouseout/mousedown.
*/
export function initCellHoverPopup() {
const boardEl = document.getElementById("board");
const popupLayer = document.getElementById("popup-layer");
if (!boardEl || !popupLayer) return;
let showTimer = null;
let hideTimer = null;
let lastCell = null;
let popup = null;
function hidePopup() {
if (popup) {
popup.remove();
popup = null;
}
}
function showPopup(cell) {
hidePopup();
const text = _buildCellHoverText(cell);
if (!text) return;
popup = _showFadePopup(boardEl, popupLayer, text, cell.x, cell.y);
}
boardEl.addEventListener("mouseover", (e) => {
if (game.state !== STATE.PLAYING) return;
if (document.querySelector("dialog[open]")) return;
const span =
e.target.closest?.(".cell") ??
(e.target.classList.contains("cell") ? e.target : null);
if (!span) return;
const x = parseInt(span.dataset.x, 10);
const y = parseInt(span.dataset.y, 10);
if (isNaN(x) || isNaN(y)) return;
const cell = game.board?.cells?.[x]?.[y];
if (!cell) return;
// Same cell — don't reset the timer (handles animated-cell mouseover storms)
if (cell === lastCell) return;
clearTimeout(showTimer);
clearTimeout(hideTimer);
hidePopup();
lastCell = cell;
showTimer = setTimeout(() => {
showPopup(cell);
hideTimer = setTimeout(() => {
hidePopup();
lastCell = null;
}, 2100);
}, 600);
});
boardEl.addEventListener("mouseout", (e) => {
// Only clear when the mouse leaves the board entirely
if (!boardEl.contains(e.relatedTarget)) {
clearTimeout(showTimer);
clearTimeout(hideTimer);
hidePopup();
lastCell = null;
}
});
boardEl.addEventListener("mousedown", () => {
clearTimeout(showTimer);
clearTimeout(hideTimer);
hidePopup();
lastCell = null;
});
}
/**
* Build the HTML legend for a hovered cell: terrain, then agent (if any),
* then the topmost item (unless the terrain has HIDES_ITEMS).
* Mirrors Java's CellInfoPanel.renderCellInfo().
* @param {Cell} cell
* @returns {string|null}
*/
function _buildCellHoverText(cell) {
const terrain = cell.getApparentTerrain?.() ?? cell.terrain;
if (!terrain) return null;
const outside = game.board?.outside ?? false;
const lines = [_renderPieceLabel(terrain, outside)];
if (cell.agent && !cell.agent.is(PLAYER)) {
lines.push(_renderPieceLabel(cell.agent, outside));
}
if (!cell.isBagEmpty && terrain.not(HIDES_ITEMS)) {
const topItem = cell.items[cell.items.length - 1];
const count = cell.items.filter((i) => i.name === topItem.name).length;
const countStr = count > 1 ? ` (x${count})` : "";
lines.push(_renderPieceLabel(topItem, outside) + countStr);
}
return lines.join("<br>");
}
/**
* Render a colored symbol + name label for one piece.
* @param {Piece} piece
* @param {boolean} outside
* @returns {string}
*/
function _renderPieceLabel(piece, outside) {
const sym = piece.symbol;
const fg = sym?.getColor?.(outside)?.hex ?? sym?.color?.hex ?? "inherit";
const bg =
sym?.getBackground?.(outside)?.hex ?? sym?.background?.hex ?? "transparent";
return `<span style="color:${fg};background:${bg}">${sym?.entity ?? "?"}</span> ${piece.name}`;
}