core/events.js

/**
 * Typed publish/subscribe event bus used to decouple the game model from the
 * view. Mirrors the Java Events singleton, simplified to plain callbacks.
 *
 * There are six event channels:
 *   game      — game lifecycle (paused, resumed)
 *   player    — player stats changed
 *   inventory — player bag changed
 *   flags     — an agent's flags changed
 *   cell      — a board cell's visual state changed
 *   message   — messages displayed to the player
 *
 * All channels support multiple subscribers.
 */

/** @typedef {function(): void} VoidFn */
/** @typedef {function(Player): void} PlayerFn */
/** @typedef {function(Player): void} InventoryFn */
/** @typedef {function(Agent): void} FlagsFn */
/** @typedef {function(Cell): void} CellFn */
/** @typedef {function(Cell, string): void} MessageFn */
/** @typedef {function(string): void} ModalFn */

class Events {
  constructor() {
    /** @type {VoidFn[]} */
    this._gamePaused = [];
    /** @type {VoidFn[]} */
    this._gameResumed = [];
    /** @type {PlayerFn[]} */
    this._playerChanged = [];
    /** @type {InventoryFn[]} */
    this._inventoryChanged = [];
    /** @type {FlagsFn[]} */
    this._flagsChanged = [];
    /** @type {CellFn[]} */
    this._cellChanged = [];
    /** @type {MessageFn[]} */
    this._message = [];
    /** @type {ModalFn[]} */
    this._modalMessage = [];
    /** @type {CellFn[]} */
    this._clearCell = [];
    /** @type {VoidFn[]} */
    this._clearCurrentCell = [];
    /** @type {VoidFn[]} */
    this._clearAllCells = [];
    /** @type {VoidFn[]} */
    this._handleInventoryMessaging = [];
    /** @type {VoidFn[]} */
    this._handleModalMessage = [];
    /** @type {function(number, number): void[]} */
    this._fallThrough = [];
  }

  // ── Registration ──────────────────────────────────────────────────────────

  /** @param {VoidFn} fn */
  onGamePaused(fn) {
    this._gamePaused.push(fn);
  }
  /** @param {VoidFn} fn */
  onGameResumed(fn) {
    this._gameResumed.push(fn);
  }
  /** @param {PlayerFn} fn */
  onPlayerChanged(fn) {
    this._playerChanged.push(fn);
  }
  /** @param {InventoryFn} fn */
  onInventoryChanged(fn) {
    this._inventoryChanged.push(fn);
  }
  /** @param {FlagsFn} fn */
  onFlagsChanged(fn) {
    this._flagsChanged.push(fn);
  }
  /** @param {CellFn} fn */
  onCellChanged(fn) {
    this._cellChanged.push(fn);
  }
  /** @param {MessageFn} fn */
  onMessage(fn) {
    this._message.push(fn);
  }
  /** @param {ModalFn} fn */
  onModalMessage(fn) {
    this._modalMessage.push(fn);
  }
  /** @param {CellFn} fn */
  onClearCell(fn) {
    this._clearCell.push(fn);
  }
  /** @param {VoidFn} fn */
  onClearCurrentCell(fn) {
    this._clearCurrentCell.push(fn);
  }
  /** @param {VoidFn} fn */
  onClearAllCells(fn) {
    this._clearAllCells.push(fn);
  }
  /** @param {VoidFn} fn */
  onHandleInventoryMessaging(fn) {
    this._handleInventoryMessaging.push(fn);
  }
  /** @param {VoidFn} fn */
  onHandleModalMessage(fn) {
    this._handleModalMessage.push(fn);
  }
  /** @param {function(number, number): void} fn */
  onFallThrough(fn) {
    this._fallThrough.push(fn);
  }

  // ── Firing ────────────────────────────────────────────────────────────────

  fireGamePaused() {
    for (const fn of this._gamePaused) fn();
  }

  fireGameResumed() {
    for (const fn of this._gameResumed) fn();
  }

  /** @param {Player} player */
  firePlayerChanged(player) {
    for (const fn of this._playerChanged) fn(player);
  }

  /** @param {Player} bag */
  fireInventoryChanged(bag) {
    for (const fn of this._inventoryChanged) fn(bag);
  }

  /** @param {Agent} agent */
  fireFlagsChanged(agent) {
    for (const fn of this._flagsChanged) fn(agent);
  }

  /** @param {Cell} cell */
  fireCellChanged(cell) {
    for (const fn of this._cellChanged) fn(cell);
  }

  /**
   * @param {Cell} cell
   * @param {string} message
   */
  fireMessage(cell, message) {
    for (const fn of this._message) fn(cell, message);
  }

  /** @param {string} message */
  fireModalMessage(message) {
    for (const fn of this._modalMessage) fn(message);
  }

  fireHandleInventoryMessaging() {
    for (const fn of this._handleInventoryMessaging) fn();
  }

  fireClearCurrentCell() {
    for (const fn of this._clearCurrentCell) fn();
  }

  /** @param {Cell} cell */
  fireClearCell(cell) {
    for (const fn of this._clearCell) fn(cell);
  }

  fireHideAllMessages() {
    for (const fn of this._clearAllCells) fn();
  }

  fireHandleModalMessage() {
    for (const fn of this._handleModalMessage) fn();
  }

  /**
   * @param {string} boardId
   * @param {number} x
   * @param {number} y
   */
  /**
   * @param {number} x
   * @param {number} y
   */
  fireFallThrough(x, y) {
    for (const fn of this._fallThrough) fn(x, y);
  }

  /** Remove all listeners. Useful in tests and on game teardown. */
  reset() {
    this._gamePaused = [];
    this._gameResumed = [];
    this._playerChanged = [];
    this._inventoryChanged = [];
    this._flagsChanged = [];
    this._cellChanged = [];
    this._message = [];
    this._modalMessage = [];
    this._clearCell = [];
    this._clearCurrentCell = [];
    this._clearAllCells = [];
    this._handleInventoryMessaging = [];
    this._handleModalMessage = [];
    this._fallThrough = [];
  }
}

/** Global event bus singleton. */
export const events = new Events();