core/event-bus.ts

/**
 * Typed publish/subscribe event bus used to decouple the game model from the
 * view. Mirrors the Java Events singleton, simplified to plain callbacks.
 *
 * Channels (game/player/inventory/flags/cell/message plus a few UI-only
 * ones like modal messages and page scrolling) all support multiple
 * subscribers.
 */

import { Agent } from "./agent";
import { Cell } from "./cell";
import { Player, PlayerBag } from "./player";

/** A single pub/sub channel: a list of listeners, fired in order. */
class Channel<Args extends unknown[]> {
  #listeners: Array<(...args: Args) => void> = [];
  on(fn: (...args: Args) => void) {
    this.#listeners.push(fn);
  }
  fire(...args: Args) {
    for (const fn of this.#listeners) fn(...args);
  }
  reset() {
    this.#listeners = [];
  }
}

class Events {
  #gamePaused = new Channel<[]>();
  #gameResumed = new Channel<[]>();
  #playerChanged = new Channel<[Player]>();
  #inventoryChanged = new Channel<[PlayerBag]>();
  #flagsChanged = new Channel<[Agent]>();
  #cellChanged = new Channel<[Cell]>();
  #message = new Channel<[Cell, string]>();
  #modalMessage = new Channel<[string]>();
  #clearCell = new Channel<[Cell]>();
  #clearCurrentCell = new Channel<[]>();
  #clearAllCells = new Channel<[]>();
  #handleInventoryMessaging = new Channel<[]>();
  #handleModalMessage = new Channel<[]>();
  #fallThrough = new Channel<[number, number]>();
  #scrollPage = new Channel<[string, string]>();

  onGamePaused(fn: () => void) {
    this.#gamePaused.on(fn);
  }
  onGameResumed(fn: () => void) {
    this.#gameResumed.on(fn);
  }
  onPlayerChanged(fn: (p: Player) => void) {
    this.#playerChanged.on(fn);
  }
  onInventoryChanged(fn: (p: PlayerBag) => void) {
    this.#inventoryChanged.on(fn);
  }
  onFlagsChanged(fn: (a: Agent) => void) {
    this.#flagsChanged.on(fn);
  }
  onCellChanged(fn: (c: Cell) => void) {
    this.#cellChanged.on(fn);
  }
  onMessage(fn: (c: Cell, s: string) => void) {
    this.#message.on(fn);
  }
  onModalMessage(fn: (s: string) => void) {
    this.#modalMessage.on(fn);
  }
  onScrollPage(fn: (s1: string, s2: string) => void) {
    this.#scrollPage.on(fn);
  }
  onClearCell(fn: (c: Cell) => void) {
    this.#clearCell.on(fn);
  }
  onClearCurrentCell(fn: () => void) {
    this.#clearCurrentCell.on(fn);
  }
  onClearAllCells(fn: () => void) {
    this.#clearAllCells.on(fn);
  }
  onHandleInventoryMessaging(fn: () => void) {
    this.#handleInventoryMessaging.on(fn);
  }
  onHandleModalMessage(fn: () => void) {
    this.#handleModalMessage.on(fn);
  }
  onFallThrough(fn: (n1: number, n2: number) => void) {
    this.#fallThrough.on(fn);
  }
  fireGamePaused() {
    this.#gamePaused.fire();
  }
  fireGameResumed() {
    this.#gameResumed.fire();
  }
  firePlayerChanged(player: Player) {
    this.#playerChanged.fire(player);
  }
  fireInventoryChanged(bag: PlayerBag) {
    this.#inventoryChanged.fire(bag);
  }
  fireFlagsChanged(agent: Agent) {
    this.#flagsChanged.fire(agent);
  }
  fireMessage(message: string, cell: Cell) {
    this.#message.fire(cell, message);
  }
  fireModalMessage(message: string) {
    this.#modalMessage.fire(message);
  }
  fireScrollPage(url: string, title: string) {
    this.#scrollPage.fire(url, title);
  }
  fireHandleInventoryMessaging() {
    this.#handleInventoryMessaging.fire();
  }
  fireClearCurrentCell() {
    this.#clearCurrentCell.fire();
  }
  fireClearCell(cell: Cell) {
    this.#clearCell.fire(cell);
  }
  fireHideAllMessages() {
    this.#clearAllCells.fire();
  }
  fireHandleModalMessage() {
    this.#handleModalMessage.fire();
  }
  fireFallThrough(x: number, y: number) {
    this.#fallThrough.fire(x, y);
  }
  /** Remove all listeners. Useful in tests and on game teardown. */
  reset() {
    this.#gamePaused.reset();
    this.#gameResumed.reset();
    this.#playerChanged.reset();
    this.#inventoryChanged.reset();
    this.#flagsChanged.reset();
    this.#cellChanged.reset();
    this.#message.reset();
    this.#modalMessage.reset();
    this.#clearCell.reset();
    this.#clearCurrentCell.reset();
    this.#clearAllCells.reset();
    this.#handleInventoryMessaging.reset();
    this.#handleModalMessage.reset();
    this.#fallThrough.reset();
    this.#scrollPage.reset();
  }
}

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