/**
* 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();