core/terrain.ts

import { Piece } from "./piece.ts";
import {
  TRAVERSABLE,
  AQUATIC,
  LAVITIC,
  WATER_RESISTANT,
  FIRE_RESISTANT,
  FLIER,
  PENETRABLE,
  PLAYER,
} from "./flags.ts";
import { Agent } from "./agent.ts";
import { Cell } from "./cell.ts";
import { Direction } from "./direction.ts";
import { GameEvent } from "./game-event.ts";
import { Player } from "./player.ts";
import { InFlightItem } from "../pieces/effects/effects.ts";
import { Item } from "./item.ts";
import { Color } from "./color.ts";

/**
 * Base class for all terrain types.
 *
 * Terrain occupies one cell and controls whether agents can enter/exit, and
 * what happens when they do. All callback methods are no-ops by default;
 * subclasses override only what they need.
 *
 * The Java Terrain interface is an event-based protocol: each callback receives
 * a GameEvent that can be cancelled to prevent the default action.
 */
export class Terrain extends Piece {
  /**
   * Can an agent (or player) enter this terrain from the given direction?
   * Mirrors Java AbstractTerrain.canEnter, plus a Raft bypass: crossing onto
   * AQUATIC terrain from a cell currently occupied by a Raft always succeeds
   * (the raft floats the agent across). WATER_RESISTANT/FIRE_RESISTANT are
   * scoped to the player only — Java handles that bypass in onEnter (which
   * this port's canEnter absorbs, since canEnter here is also consulted for
   * the player's own physical traversability check).
   * Return false to unconditionally block movement — no side effects.
   */
  canEnter(agent: Agent, cell: Cell, direction: Direction): boolean {
    if (this.is(AQUATIC)) {
      const from = cell?.getAdjacentCell(direction.reverse);
      if (from?.terrain?.name === "Raft") {
        return true;
      }
      if (agent.is(PLAYER) && agent.is(WATER_RESISTANT)) return true;
    }
    if (this.is(LAVITIC) && agent.is(PLAYER) && agent.is(FIRE_RESISTANT)) {
      return true;
    }
    return (
      this.is(TRAVERSABLE) ||
      (this.is(PENETRABLE) && agent.is(FLIER)) ||
      (this.is(LAVITIC) && agent.is(LAVITIC)) ||
      (this.is(AQUATIC) && agent.is(AQUATIC))
    );
  }

  /**
   * Can a non-player agent exit this terrain in the given direction?
   * Mirrors Java AbstractTerrain.canExit exactly (no resistance/raft bypass —
   * those only apply to entering).
   */
  canExit(agent: Agent, cell: Cell, direction: Direction): boolean {
    return (
      this.is(TRAVERSABLE) ||
      (this.is(PENETRABLE) && agent.is(FLIER)) ||
      (this.is(LAVITIC) && agent.is(LAVITIC)) ||
      (this.is(AQUATIC) && agent.is(AQUATIC))
    );
  }

  /**
   * The player is entering this terrain.
   * Cancel event to prevent the move.
   */
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) { }

  /**
   * The player is exiting this terrain.
   * Cancel event to prevent the move.
   */
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) { }

  /**
   * A non-player agent is entering this terrain.
   * Cancel event to prevent the move.
   */
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (!this.canEnter(agent, cell, dir)) {
      event.cancel();
    }
  }

  /**
   * A non-player agent is exiting this terrain.
   */
  onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) { }

  /**
   * An in-flight item is leaving this terrain (about to enter the next cell).
   * Override to destroy or absorb the item as it leaves (e.g., Waterfall).
   * @param {InFlightItem} flier
   */
  onFlyOut(event: GameEvent, _cell: Cell, _flier: InFlightItem) { }

  /**
   * An in-flight item is passing over this terrain.
   * Cancel event to make the item land here instead of continuing.
   */
  onFlyOver(event: GameEvent, _cell: Cell, _flier: InFlightItem) {
    if (!this.is(PENETRABLE)) {
      event.cancel();
    }
  }

  /**
   * An item is being dropped onto this terrain.
   * Cancel event to make the item disappear (e.g., dropping into lava).
   */
  onDrop(event: GameEvent, cell: Cell, item: Item) { }

  /**
   * An agent is picking up an item from this terrain.
   * Cancel event to prevent the pickup.
   */
  onPickup(event: GameEvent, cell: Cell, agent: Agent, item: Item) { }

  /**
   * This terrain is now adjacent to the player. Called after each move.
   * Use this to reveal secret passages, trigger proximity effects, etc.
   */
  onAdjacentTo(event: GameEvent, cell: Cell) { }

  /**
   * This terrain is no longer adjacent to the player.
   */
  onNotAdjacentTo(event: GameEvent, cell: Cell) { }

  /**
   * A color event is being broadcast on this board.
   * Implementations compare `this.color` to the event `color` parameter
   * and react only when they match.
   */
  onColorEvent(event: GameEvent, color: Color, cell: Cell) { }
}