pieces/terrain/features.ts

import {
  Arrow,
  Bullet,
  Crowbar,
  Crystal,
  Grenade,
  Key,
} from "../items/items.ts";
import { InFlightItem, oscillate, Smash } from "../effects/effects.ts";
import {
  AbstractBoulder,
  Slider,
  Pusher,
} from "../agents/creatures.ts";
import { Terrain } from "../../core/terrain.ts";
import { Registry } from "../../core/registry.ts";
import { TerrainUtils } from "../../core/terrain-utils.ts";
import { events } from "../../core/event-bus.ts";
import {
  TRAVERSABLE,
  PENETRABLE,
  VERTICAL,
  DETECT_HIDDEN,
  AMMUNITION,
  MELEE_WEAPON,
} from "../../core/flags.ts";
import {
  WHITE,
  BLACK,
  NEARBLACK,
  BARELY_BUILDING_WALL,
  BUILDING_FLOOR,
  BUILDING_WALL,
  BURLYWOOD,
  BURNTWOOD,
  DARKGOLDENROD,
  DARKGRAY,
  DARKSLATEGRAY,
  LIGHTSLATEGRAY,
  LIMEGREEN,
  LOW_ROCKS,
  RED,
  SADDLEBROWN,
  SANDYBROWN,
  SILVER,
  STEELBLUE,
  colorByName,
  NONE,
  Color,
} from "../../core/color.ts";
import { Sym } from "../../core/sym.ts";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.ts";
import { ON, OFF, stateFromString, State } from "../../core/state.ts";

import {
  directionByName,
  NONE as DIR_NONE,
  WEST,
  EAST,
  Direction,
} from "../../core/direction.ts";
import { GameEvent } from "../../core/game-event.ts";
import { Cell } from "../../core/cell.ts";
import { Agent } from "../../core/agent.ts";
import { Player } from "../../core/player.ts";
import { Item } from "../../core/item.ts";
import { Rubble } from "./decorators.ts";
import { Floor } from "./basic.ts";
import { Piece } from "../../core/piece.ts";
import { Animated } from "../../core/animated.ts";

/**
 * A door that is either open (ON) or locked (OFF). A matching color key
 * unlocks/relocks it; a color event also toggles it.
 */
export class Door extends Terrain {
  state: State;
  constructor(color: Color, state: State) {
    const sym = state.isOn()
      ? Sym.of("⸱", color, null, color, BUILDING_FLOOR)
      : Sym.of("⸱", WHITE, color, BUILDING_FLOOR, color);
    super(color.name + " Door", 0, color, sym);
    this.state = state;
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color !== this.color) return;
    TerrainUtils.toggleCellState(cell, this, this.state);
  }
  canEnter(agent: Agent, cell: Cell, direction: Direction) {
    return this.state.isOn() && !direction.isDiagonal();
  }
  canExit(agent: Agent, cell: Cell, direction: Direction) {
    return !direction.isDiagonal();
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal() || dir.isVertical()) {
      event.cancel();
      return;
    }
    const item = player.bag.getSelected();
    if (this.state.isOff()) {
      if (
        item instanceof Key &&
        item.color === this.color &&
        cell.agent == null
      ) {
        TerrainUtils.toggleCellState(cell, this, this.state);
        player.bag.remove(item);
        event.cancel("Door unlocks.", cell);
      } else {
        event.cancel("The door is locked.", cell);
      }
    } else {
      if (
        item instanceof Key &&
        item.color === this.color &&
        cell.agent == null
      ) {
        TerrainUtils.toggleCellState(cell, this, this.state);
        player.bag.remove(item);
        event.cancel("You lock the door.", cell);
      }
    }
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal() || dir.isVertical()) event.cancel();
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (agent instanceof AbstractBoulder) {
      event.cancel("It's too big.", cell);
    } else if (dir.isDiagonal() || dir.isVertical() || this.state.isOff()) {
      event.cancel();
    }
  }
  onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (dir.isDiagonal() || dir.isVertical()) event.cancel();
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (this.state.isOff() || flier.direction.isDiagonal()) event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state]: string[]) {
      return new Door(colorByName(color) ?? NONE, stateFromString(state));
    }
    store(d: Door) {
      return `Door|${d.color.name}|${d.state.name}`;
    }
    example() {
      return new Door(STEELBLUE, OFF);
    }
    template() {
      return "Door|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * A gate that agents can open by walking through it straight-on.
 * It auto-closes after the agent passes.
 */
export class Gate extends Terrain {
  state: State;
  constructor(state: State) {
    const sym = state.isOff()
      ? Sym.of("#", WHITE, null, BLACK, BUILDING_FLOOR)
      : Sym.of("#", NEARBLACK, null, BUILDING_WALL, BUILDING_FLOOR);
    super("Gate", TRAVERSABLE | PENETRABLE, NONE, sym);
    this.state = state;
  }
  canEnter(agent: Agent, cell: Cell, direction: Direction) {
    return !direction.isDiagonal();
  }
  canExit(agent: Agent, cell: Cell, direction: Direction) {
    return !direction.isDiagonal();
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal()) {
      event.cancel();
      return;
    }
    if (this.state.isOff()) {
      TerrainUtils.toggleCellState(cell, this, this.state);
      event.cancel();
      events.fireMessage("You open the gate", cell);
    }
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal()) {
      event.cancel();
      return;
    }
    TerrainUtils.toggleCellState(cell, this, this.state);
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (agent instanceof AbstractBoulder) {
      event.cancel("It's too big.", cell);
    } else if (this.state.isOff()) {
      TerrainUtils.toggleCellState(cell, this, this.state);
      event.cancel();
    }
  }
  onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    TerrainUtils.toggleCellState(cell, this, this.state);
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (flier.direction.isDiagonal()) event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([state]: string[]) {
      return new Gate(stateFromString(state));
    }
    store(g: Gate) {
      return `Gate|${g.state.name}`;
    }
    example() {
      return new Gate(ON);
    }
    template() {
      return "Gate|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/** Rusty gate — permanently open; player can enter but it's impassable for items */
class RustyGate extends Terrain {
  constructor() {
    super(
      "Rusty Gate",
      PENETRABLE,
      NONE,
      Sym.of("#", SANDYBROWN, null, SADDLEBROWN, BUILDING_FLOOR),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel("The gate is too rusty to open", cell);
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (flier.direction.isDiagonal()) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("RustyGate");
    }
    create() {
      return new RustyGate();
    }
    tag() {
      return "Room Features";
    }
  })();
}

class StairsUp extends Terrain {
  constructor() {
    super(
      "Stairs Up",
      TRAVERSABLE | PENETRABLE | VERTICAL,
      NONE,
      Sym.of("<", WHITE, null, BLACK, BUILDING_FLOOR),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    events.fireMessage("Use 'z' to go up", cell);
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isVertical() && dir.name === "down") event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("StairsUp");
    }
    create() {
      return new StairsUp();
    }
    tag() {
      return "Room Features";
    }
  })();
}

class StairsDown extends Terrain {
  constructor() {
    super(
      "Stairs Down",
      TRAVERSABLE | PENETRABLE | VERTICAL,
      NONE,
      Sym.of(">", WHITE, null, BLACK, BUILDING_FLOOR),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    events.fireMessage("Use 'z' to go down", cell);
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isVertical() && dir.name === "up") event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("StairsDown");
    }
    create() {
      return new StairsDown();
    }
    tag() {
      return "Room Features";
    }
  })();
}

class CaveEntrance extends Terrain {
  constructor() {
    super(
      "Cave Entrance",
      TRAVERSABLE | PENETRABLE | VERTICAL,
      NONE,
      Sym.of("∩", SILVER, NONE, NEARBLACK, LOW_ROCKS),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    const msg = event.board.outside
      ? "Use 'z' to enter the cave"
      : "Use 'z' to exit the cave";
    events.fireMessage(msg, cell);
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!dir.isVertical()) return;
    // Outside: can only go DOWN into the cave; inside: can only go UP out of it
    if (
      (event.board.outside && dir.name === "up") ||
      (!event.board.outside && dir.name === "down")
    ) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("CaveEntrance");
    }
    create() {
      return new CaveEntrance();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Locked chest. Requires a matching color key.
 * item and count may be null (empty chest).
 */
export class Chest extends Terrain {
  item: Item | null;
  count: number;
  constructor(item: Item | null, count: number, color: Color) {
    if (color === NONE) {
      throw new Error("Chest must have a color");
    }
    super(
      `${color.name} Chest`,
      PENETRABLE,
      color,
      Sym.of("⊠", color, null, color, BUILDING_FLOOR),
    );
    this.item = item;
    this.count = count;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!cell.isBagEmpty) return;
    const sel = player.bag.getSelected();
    if (sel instanceof Key) {
      if (sel.color === this.color) {
        cell.openContainer(
          "chest",
          this.item,
          this.count,
          "EmptyChest",
          player,
        );
        player.bag.remove(sel);
      } else {
        events.fireMessage("The key is not the right color", cell);
      }
    } else {
      events.fireMessage("A large locked chest blocks your way", cell);
    }
    event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      if (args.length === 1)
        return new Chest(null, 1, colorByName(args[0]) ?? NONE);
      if (args.length === 2)
        return new Chest(Registry.item(args[0]), 1, colorByName(args[1]) ?? NONE);
      return new Chest(
        Registry.item(args[0]),
        parseInt(args[1]) || 1,
        colorByName(args[2]) ?? NONE,
      );
    }
    store(c: Chest) {
      return c.item
        ? `Chest|${this.esc(c.item)}|${c.count}|${c.color.name}`
        : `Chest|${c.color.name}`;
    }
    example() {
      return new Chest(null, 1, STEELBLUE);
    }
    template() {
      return "Chest|{item?}|{count?}|{color}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Board-Strewn Floor — the remnants of a smashed crate. Traversable.
 * Mirrors Java's Boards (OpeningMarker subclass).
 */
class Boards extends Terrain {
  constructor() {
    super(
      "Board-Strewn Floor",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("≠", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Boards");
    }
    create() {
      return new Boards();
    }
    tag() {
      return "Room Features";
    }
  })();
}

/** Empty chest — traversable floor tile with hollow chest appearance */
class EmptyChest extends Terrain {
  constructor() {
    super(
      "Empty Chest",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("⊔", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("EmptyChest");
    }
    create() {
      return new EmptyChest();
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Crate — requires a crowbar to open.
 * item and count may be null.
 */
export class Crate extends Terrain {
  item: Item | null;
  count: number;
  constructor(item: Item | null, count: number | null) {
    super(
      "Crate",
      PENETRABLE,
      NONE,
      Sym.of("⊠", WHITE, null, NEARBLACK, BUILDING_FLOOR),
    );
    this.item = item;
    this.count = count ?? 1
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!cell.isBagEmpty) return;
    const sel = player.bag.getSelected();
    if (sel instanceof Crowbar) {
      cell.openContainer("crate", this.item, this.count, "Boards", player);
    } else {
      events.fireMessage("A large crate. It can't be moved, but try prying it open", cell);
    }
    event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      if (args.length === 0) return new Crate(null, 1);
      if (args.length === 1) return new Crate(Registry.item(args[0]), 1);
      return new Crate(Registry.item(args[0]), parseInt(args[1]) || 1);
    }
    store(c: Crate) {
      return c.item ? `Crate|${this.esc(c.item)}|${c.count}` : "Crate";
    }
    example() {
      return new Crate(null, 1);
    }
    template() {
      return "Crate|{item}|{count?}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Urn — smashable container. Requires a melee weapon to smash; projectiles
 * (arrows, bullets) also smash it. Matches Java Urn behavior.
 */
export class Urn extends Terrain {
  #item: Item | null;
  constructor(item: Item | null) {
    super(
      "Urn",
      0,
      NONE,
      Sym.of("𐃡", DARKGOLDENROD, BLACK, DARKGOLDENROD, BUILDING_FLOOR),
    );
    this.#item = item;
  }
  get item() {
    return this.#item;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    const sel = player.bag.getSelected();
    if (sel.is(MELEE_WEAPON)) {
      events.fireMessage("You smash open the urn", cell);
      this.#smashUrn(cell);
    } else {
      events.fireMessage("The urn is too large", cell);
    }
    event.cancel();
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (flier.item instanceof Arrow || flier.item instanceof Bullet) {
      event.cancel();
      this.#smashUrn(cell);
    }
  }
  #smashUrn(cell: Cell) {
    if (this.#item !== null) {
      cell.addItem(this.#item);
    }
    cell.addEffect(new Smash());
    cell.setTerrain(Registry.terrain("Rubble|Floor"));
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new Urn(args.length > 0 ? Registry.item(args[0]) : null);
    }
    store(u: Urn) {
      return u.#item ? `Urn|${this.esc(u.#item)}` : "Urn";
    }
    example() {
      return new Urn(null);
    }
    template() {
      return "Urn|{item?}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Switch — fires a color event; toggles appearance.
 * Can be triggered by throwing a non-ammunition item at it.
 */
export class Switch extends Terrain {
  state: State;
  constructor(color: Color, state: State) {
    const entity = state.isOn() ? "!" : "\u00A1"; // ¡
    super(color.name + " Switch", 0, color, Sym.of(entity, BLACK, color));
    this.state = state;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    if (dir.isDiagonal()) {
      events.fireMessage("You must use it square on", cell);
      return;
    }
    TerrainUtils.toggleCellState(cell, this, this.state);
    event.board.fireColorEvent(event, this.color, cell);
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    event.cancel();
    if (!flier.item.is(AMMUNITION)) {
      TerrainUtils.toggleCellState(cell, this, this.state);
      event.board.fireColorEvent(event, this.color, cell);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state]: string[]) {
      return new Switch(colorByName(color) ?? NONE, stateFromString(state));
    }
    store(s: Switch) {
      return `Switch|${s.color.name}|${s.state.name}`;
    }
    example() {
      return new Switch(STEELBLUE, ON);
    }
    template() {
      return "Switch|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * KeySwitch — fires a color event only when the player uses a matching color key.
 */
export class KeySwitch extends Terrain {
  state: State;
  constructor(color: Color, state: State) {
    const entity = state.isOn() ? "?" : "\u00BF"; // ¿
    super(color.name + " Key Switch", 0, color, Sym.of(entity, BLACK, color));
    this.state = state;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    if (dir.isDiagonal()) {
      events.fireMessage("You must use it square on", cell);
      return;
    }
    const sel = player.bag.getSelected();
    if (sel instanceof Key) {
      if (sel.color === this.color) {
        TerrainUtils.toggleCellState(cell, this, this.state);
        event.board.fireColorEvent(event, this.color, cell);
        player.bag.remove(sel);
      } else {
        events.fireMessage("That key is the wrong color", cell);
      }
    } else {
      events.fireMessage("This switch requires a key to operate", cell);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state]: string[]) {
      return new KeySwitch(colorByName(color) ?? NONE, stateFromString(state));
    }
    store(ks: KeySwitch) {
      return `KeySwitch|${ks.color.name}|${ks.state.name}`;
    }
    example() {
      return new KeySwitch(STEELBLUE, ON);
    }
    template() {
      return "KeySwitch|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * PressurePlate — fires a color event when any agent steps on or off it.
 */
export class PressurePlate extends Terrain {
  constructor(color: Color) {
    super(
      "Pressure Plate",
      TRAVERSABLE | PENETRABLE,
      color,
      Sym.of("Ξ", NEARBLACK, BLACK, BARELY_BUILDING_WALL, BUILDING_FLOOR),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!event.isCancelled)
      event.board.fireColorEvent(event, this.color, cell);
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!event.isCancelled)
      event.board.fireColorEvent(event, this.color, cell);
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (!event.isCancelled)
      event.board.fireColorEvent(event, this.color, cell);
  }
  onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (!event.isCancelled)
      event.board.fireColorEvent(event, this.color, cell);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]: string[]) {
      return new PressurePlate(colorByName(color) ?? NONE);
    }
    store(pp: PressurePlate) {
      return `PressurePlate|${pp.color.name}`;
    }
    example() {
      return new PressurePlate(RED);
    }
    template() {
      return "PressurePlate|{color}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Altar — cosmetic 3-part piece (WEST bracket, NONE center π, EAST bracket).
 * Pass direction string "west", "none", or "east".
 */
export class Altar extends Terrain {
  #direction: Direction;
  constructor(direction: Direction) {
    let entity, fg;
    if (direction === WEST || direction.name === "west") {
      entity = "[";
      fg = DARKGRAY;
    } else if (direction === EAST || direction.name === "east") {
      entity = "]";
      fg = DARKGRAY;
    } else {
      entity = "π";
      fg = LIGHTSLATEGRAY;
    }
    super("Altar", TRAVERSABLE | PENETRABLE, NONE, Sym.of(entity, fg, DARKSLATEGRAY));
    this.#direction = direction;
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([direction]: string[]) {
      return new Altar(directionByName(direction) ?? DIR_NONE);
    }
    store(a: Altar) {
      return `Altar|${a.#direction.name}`;
    }
    example() {
      return new Altar(WEST);
    }
    template() {
      return "Altar|{direction}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Bookshelf — impassable; searching gives the stored item.
 * item may be null (empty shelf).
 */
export class Bookshelf extends Terrain {
  static UNMARKED_SYM = Sym.of(
    "≣",
    BURLYWOOD,
    BLACK,
    BURNTWOOD,
    BUILDING_FLOOR,
  );
  static MARKED_SYM = Sym.of("≡", LIMEGREEN, BLACK, LIMEGREEN, BUILDING_FLOOR);
  item: Item | null;

  constructor(item: Item | null) {
    super("Bookshelf", 0, NONE, Bookshelf.UNMARKED_SYM);
    this.item = item;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal()) {
      event.cancel("It's a little too far to reach.");
      return;
    }
    event.cancel();
    if (this.item != null) {
      events.fireModalMessage(
        this.item.getIndefiniteNoun("Searching the bookshelf, you find {0}"),
      );
      player.bag.add(this.item);
      // Replace with empty bookshelf
      cell.setTerrain(Registry.terrain("Bookshelf"));
    } else {
      events.fireMessage("You find nothing on the bookshelf.", cell);
    }
  }
  onAdjacentTo(event: GameEvent, cell: Cell) {
    if (this.item != null && event.player.is(DETECT_HIDDEN)) {
      cell.animTerrainSymbol = Bookshelf.MARKED_SYM;
      cell.board.notifyCellChange(cell);
    }
  }
  onNotAdjacentTo(event: GameEvent, cell: Cell) {
    cell.animTerrainSymbol = null;
    cell.board.notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new Bookshelf(args.length > 0 ? Registry.item(args[0]) as Item : null);
    }
    store(b: Bookshelf) {
      return b.item ? `Bookshelf|${this.esc(b.item)}` : "Bookshelf";
    }
    example() {
      return new Bookshelf(null);
    }
    template() {
      return "Bookshelf|{item?}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Pit — impassable. Boulders fill it; other items fall in (disappear).
 */
export class Pit extends Terrain {
  constructor() {
    super(
      "Pit",
      PENETRABLE,
      NONE,
      Sym.of("O", NEARBLACK, BLACK, BLACK, BUILDING_FLOOR),
    );
  }
  canEnter(agent: Agent, cell: Cell, direction: Direction) {
    return agent instanceof AbstractBoulder;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel("You'd fall into the pit", cell);
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (agent instanceof AbstractBoulder) {
      const prevCell = cell.getAdjacentCell(dir.reverse);
      prevCell?.removeAgent(agent);
      cell.setTerrain(Registry.terrain("Floor"));
      events.fireMessage("The pit is filled by the boulder", cell);
    } else if (agent instanceof Slider || agent instanceof Pusher) {
      const prevCell = cell.getAdjacentCell(dir.reverse);
      prevCell?.removeAgent(agent);
      events.fireMessage(`The ${agent.name.toLowerCase()} falls through the pit`, cell);
    } else {
      event.cancel();
    }
  }
  onDrop(event: GameEvent, cell: Cell, item: Item) {
    if (!(item instanceof Grenade)) {
      event.cancel(
        item.getDefiniteNoun("{0} falls into the pit"),
        cell
      );
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Pit");
    }
    create() {
      return new Pit();
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Pylon — a color-keyed teleporter. When OFF, can only be activated by
 * consuming a matching Crystal. When ON, teleports the player to the
 * configured destination. Stepping off an inactive pylon auto-activates it,
 * enabling two-way travel with a single crystal.
 */
export class Pylon extends Terrain implements Animated {
  state: State;
  #boardID: string;
  #x: number;
  #y: number;
  constructor(color: Color, state: State, boardID: string, x: number, y: number) {
    super(
      color.name + " Pylon",
      0,
      color,
      Sym.of("Δ", color, null, color, BUILDING_FLOOR),
    );
    this.state = state;
    this.#boardID = boardID;
    this.#x = x;
    this.#y = y;
  }
  animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation {
    throw new Error("Method not implemented.");
  }
  getAnimations(options?: GetAnimationsOptions): Animation[] {
    throw new Error("Method not implemented.");
  }
  randomSeed() {
    return true;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (this.state.isOff()) {
      event.cancel();
      const item = player.bag.getSelected();
      if (item instanceof Key && item.color === this.color) {
        event.cancel("Pylons cannot be activated with keys.", cell);
      } else if (item instanceof Crystal && item.color === this.color) {
        player.bag.remove(item);
        TerrainUtils.toggleCellState(cell, this, this.state);
      } else {
        event.cancel("The pylon must be activated.", cell);
      }
    } else if (dir !== DIR_NONE) {
      // dir is Direction.NONE when landing on the pylon (e.g. arriving via
      // teleport), which is when we don't want to teleport again.
      event.game.teleport(event, dir, this.#boardID, this.#x, this.#y);
    }
  }
  onExit(_event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (this.state.isOff()) {
      const other = TerrainUtils.getTerrainOtherState(cell.terrain!, this.state);
      cell.setTerrain(other);
    }
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (this.state.isOn()) {
      const outside = cell.board.outside;
      const bg = cell.terrain.symbol.getBackground(outside);
      if (bg) {
        const nfg = oscillate(this.color, NONE, 15, frame);
        const nbg = oscillate(bg, this.color, 15, frame);
        cell.animTerrainSymbol = Sym.of("Δ", nfg, nbg, nfg, nbg);
        cell.board.notifyCellChange(cell);
      }
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state, boardID, x, y]: string[]) {
      return new Pylon(
        colorByName(color) ?? NONE,
        stateFromString(state),
        boardID,
        parseInt(x) || 0,
        parseInt(y) || 0,
      );
    }
    store(p: Pylon) {
      return `Pylon|${p.color.name}|${p.state.name}|${p.#boardID}|${p.#x}|${p.#y}`;
    }
    example() {
      return new Pylon(STEELBLUE, OFF, "", 0, 0);
    }
    template() {
      return "Pylon|{color}|{state}|{boardID}|{x}|{y}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

export function registerFeaturesTerrain() {
  Registry.register("Door", Door.SERIALIZER);
  Registry.register("Gate", Gate.SERIALIZER);
  Registry.register("RustyGate", RustyGate.SERIALIZER);
  Registry.register("StairsUp", StairsUp.SERIALIZER);
  Registry.register("StairsDown", StairsDown.SERIALIZER);
  Registry.register("CaveEntrance", CaveEntrance.SERIALIZER);
  Registry.register("Boards", Boards.SERIALIZER);
  Registry.register("EmptyChest", EmptyChest.SERIALIZER);
  Registry.register("Chest", Chest.SERIALIZER);
  Registry.register("Crate", Crate.SERIALIZER);
  Registry.register("Urn", Urn.SERIALIZER);
  Registry.register("Switch", Switch.SERIALIZER);
  Registry.register("KeySwitch", KeySwitch.SERIALIZER);
  Registry.register("PressurePlate", PressurePlate.SERIALIZER);
  Registry.register("Altar", Altar.SERIALIZER);
  Registry.register("Bookshelf", Bookshelf.SERIALIZER);
  Registry.register("Pit", Pit.SERIALIZER);
  Registry.register("Pylon", Pylon.SERIALIZER);
}