import { COLUMNS, ROWS } from "../core/board.js";
/**
* CSS-grid board renderer.
*
* Manages the 40×25 grid of `.cell` <span> elements inside `#board`.
* The DOM order is row-major — element index i corresponds to
* x = i % COLUMNS, y = Math.floor(i / COLUMNS)
* which means we fill left-to-right, top-to-bottom as CSS grid expects.
*
* Each cell span is kept in a flat array `_els[y * COLUMNS + x]` so we
* can index by (x, y) without a 2D array.
*/
export class BoardView {
/**
* @param {HTMLElement} boardEl The `#board` container element.
*/
constructor(boardEl) {
this._el = boardEl;
this._els = [];
this._board = null;
}
// ── Initialisation ────────────────────────────────────────────────────────
/**
* Attach the view to a board: build DOM elements, subscribe to changes,
* and render the initial state.
* @param {Board} board
*/
attach(board) {
this._board = board;
this._buildDom();
board.onCellChange((cell) => this._updateCell(cell));
this._renderAll();
}
/** Detach from the current board (on board-to-board navigation). */
detach() {
this._board = null;
// DOM elements are rebuilt on next attach()
}
// ── DOM construction ──────────────────────────────────────────────────────
/** (Re)build the grid of span elements. */
_buildDom() {
this._el.textContent = ""; // clear previous content
this._els = new Array(COLUMNS * ROWS);
const frag = document.createDocumentFragment();
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
const span = document.createElement("span");
span.className = "cell";
span.dataset.x = x;
span.dataset.y = y;
frag.appendChild(span);
this._els[y * COLUMNS + x] = span;
}
}
this._el.appendChild(frag);
}
// ── Rendering ─────────────────────────────────────────────────────────────
/** Re-render every cell on the board (called after attach or board swap). */
_renderAll() {
if (!this._board) return;
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
this._updateCell(this._board.cells[x][y]);
}
}
}
/**
* Update the span for a single cell.
* @param {Cell} cell
*/
_updateCell(cell) {
const span = this._els[cell.y * COLUMNS + cell.x];
if (!span) return;
const outside = this._board.outside;
const { entity, fg, bg } = cell.getDisplaySymbol(outside);
span.textContent = entity ?? " ";
span.style.color = fg != null ? fg : "transparent";
span.style.backgroundColor = bg != null ? bg : "transparent";
// Mark the player's current cell with the cursor class
const isPlayer = cell.containsPlayer();
span.classList.toggle("cursor", isPlayer);
}
// ── Public helpers ────────────────────────────────────────────────────────
/**
* Force a full re-render (e.g. after the outside/inside flag changes).
*/
rerender() {
this._renderAll();
}
/**
* Highlight the cell at (x, y) as the cursor location (removes the
* previous highlight automatically on the next cell update cycle).
* @param {number} x
* @param {number} y
*/
moveCursorTo(x, y) {
// Clear all cursor classes
for (const span of this._els) {
span.classList.remove("cursor");
}
const span = this._els[y * COLUMNS + x];
if (span) span.classList.add("cursor");
}
}