pieces/terrain/basic.ts

import { Terrain } from "../../core/terrain.ts";
import { Registry } from "../../core/registry.ts";
import {
  Bullet,
  EuclideanShard,
  Fish,
  FishingPole,
  GoldCoin,
  Grenade,
  Rock,
} from "../items/items.ts";
import { TerrainUtils } from "../../core/terrain-utils.ts";
import { events } from "../../core/event-bus.ts";
import { Farthapod, ImmobileAgent, KillerBee } from "../agents/creatures.ts";
import {
  TRAVERSABLE,
  PENETRABLE,
  ETHEREAL,
  HIDES_ITEMS,
  AQUATIC,
  POISONED,
  WEAK,
  AMMO_ENLIVENER,
} from "../../core/flags.ts";
import {
  BLACK,
  BLUE,
  BUILDING_FLOOR,
  BUILDING_WALL,
  BURLYWOOD,
  BURNTWOOD,
  BUSHES,
  CLIFFS,
  DARK_PIER,
  DARKGOLDENROD,
  DARKKHAKI,
  DARKSLATEBLUE,
  DARKSLATEGRAY,
  FOREST,
  FORESTGREEN,
  GHOSTWHITE,
  GOLD,
  GOLDENROD,
  GRASS,
  HIGH_ROCKS,
  LESSNEARBLACK,
  LIGHTSLATEGRAY,
  LIGHTSTEELBLUE,
  LOW_ROCKS,
  NEARBLACK,
  NONE,
  OCEAN,
  ORANGE,
  PIER,
  RED,
  SALMON,
  SAND,
  SIENNA,
  SILVER,
  SKYBLUE,
  SURF,
  TAN,
  VERYBLACK,
  VIOLET,
  WHITE,
  WOOD_PILING,
  YELLOW,
  colorByName,
  Color,
} from "../../core/color.ts";
import { Sym } from "../../core/sym.ts";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.ts";
import { stateFromString, ON, OFF, State } from "../../core/state.ts";
import {
  directionByName,
  NORTH,
  SOUTH,
  EAST,
  WEST,
  NORTHEAST,
  NORTHWEST,
  SOUTHEAST,
  SOUTHWEST,
  NONE as DIR_NONE,
  Direction,
} from "../../core/direction.ts";
import { game } from "../../core/game.ts";
import { getRandomDirection } from "../agents/targeting.ts";
import { Player, MAX_HEALTH } from "../../core/player.ts";
import { Cell } from "../../core/cell.ts";
import { GameEvent } from "../../core/game-event.ts";
import { Item } from "../../core/item.ts";
import { InFlightItem } from "../effects/effects.ts";
import { Agent } from "../../core/agent.ts";
import { Piece } from "../../core/piece.ts";
import { Animated } from "../../core/animated.ts";

const EMPTY_HANDED = "Empty-handed";

export class Floor extends Terrain {
  constructor() {
    super(
      "Floor",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("\u2003", VERYBLACK, BLACK, BUILDING_FLOOR, BUILDING_FLOOR),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Floor");
    }
    create() {
      return new Floor();
    }
    tag() {
      return "Terrain";
    }
  })();
}

export class Wall extends Terrain {
  constructor() {
    super("Wall", 0, NONE, Sym.of("\u2003", NONE, NEARBLACK, null, BUILDING_WALL));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Wall");
    }
    create() {
      return new Wall();
    }
    tag() {
      return "Terrain";
    }
  })();
}

class Dirt extends Terrain {
  constructor() {
    super("Dirt", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, TAN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Dirt");
    }
    create() {
      return new Dirt();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class Field extends Terrain {
  constructor() {
    super("Field", TRAVERSABLE | PENETRABLE, NONE, Sym.of("„", FORESTGREEN, TAN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Field");
    }
    create() {
      return new Field();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class Forest extends Terrain {
  constructor() {
    super("Forest", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, FOREST));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Forest");
    }
    create() {
      return new Forest();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Crevasse — an impassable chasm. Appears as a plain black cell until the
 * player is adjacent, at which point it reveals itself as "≈" on a very-dark
 * background. Has no outside representation (looks the same in both modes).
 */
class Crevasse extends Terrain {
  static REVEALED = Sym.of("\u2248", BLACK, VERYBLACK);
  static NOT_REVEALED = Sym.of("\u2003", BLACK, BLACK);

  constructor() {
    super("Crevasse", ETHEREAL | PENETRABLE, NONE, Crevasse.NOT_REVEALED);
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel("A deep crevasse spans before you", cell);
  }
  onDrop(event: GameEvent, cell: Cell, item: Item) {
    if (!(item instanceof Grenade)) {
      event.cancel();
    }
  }
  onAdjacentTo(event: GameEvent, cell: Cell) {
    cell.animTerrainSymbol = Crevasse.REVEALED;
    cell.board.notifyCellChange(cell);
  }
  onNotAdjacentTo(event: GameEvent, cell: Cell) {
    cell.animTerrainSymbol = null;
    cell.board.notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Crevasse");
    }
    create() {
      return new Crevasse();
    }
    tag() {
      return "Terrain";
    }
  })();
}

/** Sky — ethereal: only fliers can move here, but items pass through */
class Sky extends Terrain {
  constructor() {
    super("Sky", ETHEREAL | PENETRABLE, NONE, Sym.of("\u2003", NONE, SKYBLUE));
  }
  onDrop(event: GameEvent, cell: Cell, item: Item) {
    if (!(item instanceof Grenade)) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Sky");
    }
    create() {
      return new Sky();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** Cloud — traversable; items cannot be dropped here */
class Cloud extends Terrain {
  constructor() {
    super(
      "Cloud",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("\u2003", NONE, GHOSTWHITE),
    );
  }
  onDrop(event: GameEvent, cell: Cell, item: Item) {
    event.cancel(`The ${item.name} falls through the clouds...`, cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Cloud");
    }
    create() {
      return new Cloud();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

export class ChalkedFloor extends Terrain {
  constructor() {
    super(
      "Chalked Floor",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("✗", LESSNEARBLACK, null, NEARBLACK, BUILDING_FLOOR),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("ChalkedFloor");
    }
    create() {
      return new ChalkedFloor();
    }
    tag() {
      return "Terrain";
    }
  })();
}

/** TrashPile — HIDES_ITEMS: items are not visible until you step on it */
class TrashPile extends Terrain {
  constructor() {
    super(
      "Trash Pile",
      TRAVERSABLE | PENETRABLE | HIDES_ITEMS,
      NONE,
      Sym.of("%", SIENNA, BLACK, SIENNA, BUILDING_FLOOR),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("TrashPile");
    }
    create() {
      return new TrashPile();
    }
    tag() {
      return "Room Features";
    }
  })();
}

/** Haystack — HIDES_ITEMS, with outdoor color variant */
class Haystack extends Terrain {
  constructor() {
    super(
      "Haystack",
      TRAVERSABLE | PENETRABLE | HIDES_ITEMS,
      NONE,
      Sym.of("λ", GOLDENROD, BLACK, DARKGOLDENROD, TAN),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Haystack");
    }
    create() {
      return new Haystack();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class Pier extends Terrain {
  constructor() {
    super("Pier", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, PIER));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Pier");
    }
    create() {
      return new Pier();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class WoodPiling extends Terrain {
  constructor() {
    super("Wood Piling", 0, NONE, Sym.of("\u2003", NONE, WOOD_PILING));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("WoodPiling");
    }
    create() {
      return new WoodPiling();
    }
    tag() {
      return "Terrain";
    }
  })();
}

class Sand extends Terrain {
  constructor() {
    super("Sand", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, SAND));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Sand");
    }
    create() {
      return new Sand();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class LowRocks extends Terrain {
  constructor() {
    super(
      "Low Rocks",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("\u2003", NONE, LOW_ROCKS),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("LowRocks");
    }
    create() {
      return new LowRocks();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** HighRocks — TRAVERSABLE; blocks in-flight items (arrows etc.) */
class HighRocks extends Terrain {
  constructor() {
    super(
      "High Rocks",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("ᨒ", HIGH_ROCKS, LOW_ROCKS), // alt: ^
    );
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    event.cancel();
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("HighRocks");
    }
    create() {
      return new HighRocks();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** ImpassableCliffs — totally impassable rock face */
class ImpassableCliffs extends Terrain {
  constructor() {
    super("Impassable Cliffs", 0, NONE, Sym.of("ᨏ", BLACK, HIGH_ROCKS));
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel("The rocks are too steep to climb here.", cell);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("ImpassableCliffs");
    }
    create() {
      return new ImpassableCliffs();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** PyramidWall — impassable, tinted with a designer-chosen color */
export class PyramidWall extends Terrain {
  constructor(color: Color) {
    super(
      color.name + " Pyramid Wall",
      0,
      color,
      Sym.of("\u2003", NONE, color),
    );
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]: string[]) {
      return new PyramidWall(colorByName(color) ?? NONE);
    }
    store(pw: PyramidWall) {
      return `PyramidWall|${pw.color.name}`;
    }
    example() {
      return new PyramidWall(NONE);
    }
    template() {
      return "PyramidWall|{color}";
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** Fence — purely cosmetic barrier (PENETRABLE only, no TRAVERSABLE) */
export class Fence extends ImmobileAgent {
  direction: Direction;
  constructor(direction: Direction) {
    if (direction != NORTH && direction != EAST && direction != DIR_NONE) {
      throw new Error("Fence direction must be north, east or none.");
    }
    const symbol = Sym.of(
      direction == EAST ? "=" : direction == NORTH ? "I" : "‡",
      BURLYWOOD,
      null,
      BURNTWOOD,
      null,
    );
    super("Fence", PENETRABLE, symbol);
    this.direction = direction;
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([dir]: string[]) {
      return new Fence(directionByName(dir) ?? NORTH);
    }
    store(f: Fence) {
      return `Fence|${f.direction.name}`;
    }
    example() {
      return new Fence(NORTH);
    }
    template() {
      return "Fence|{direction}";
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Bridge — only N/S/E/W movement allowed; diagonal and vertical blocked.
 * The background mirrors the terrain below it (water, lava, etc.).
 */
export class Bridge extends Terrain {
  terrain: Terrain;
  constructor(terrain: Terrain) {
    super(
      "Bridge",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of(
        "䷀",
        DARK_PIER,
        terrain.symbol.getBackground(false),
        PIER,
        terrain.symbol.getBackground(true),
      ),
    );
    this.terrain = terrain;
  }
  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() || dir.isVertical()) event.cancel();
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir.isDiagonal() || dir.isVertical()) event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new Bridge(Registry.terrain(args[0]) as Terrain);
    }
    store(b: Bridge) {
      return `Bridge|${this.esc(b.terrain)}`;
    }
    example() {
      return new Bridge(new Floor());
    }
    template() {
      return "Bridge|{terrain}";
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** Flowers — decorative ground cover tinted with a color */
export class Flowers extends Terrain {
  constructor(color: Color) {
    super(
      color.name + " Flowers",
      TRAVERSABLE | PENETRABLE,
      color,
      Sym.of("⚘" /*"ϊ"*/, color, GRASS),
    );
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]: string[]) {
      return new Flowers(colorByName(color) ?? NONE);
    }
    store(f: Flowers) {
      return `Flowers|${f.color.name}`;
    }
    example() {
      return new Flowers(NONE);
    }
    template() {
      return "Flowers|{color}";
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class Grass extends Terrain {
  constructor() {
    super("Grass", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, GRASS));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Grass");
    }
    create() {
      return new Grass();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

class TallGrass extends Terrain {
  constructor() {
    super("Tall Grass", TRAVERSABLE | PENETRABLE, NONE, Sym.of("…", BUSHES, GRASS));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("TallGrass");
    }
    create() {
      return new TallGrass();
    }
    tag() {
      return "Grasses";
    }
  })();
}

class BunchGrass extends Terrain {
  constructor() {
    super("Bunch Grass", TRAVERSABLE | PENETRABLE, NONE, Sym.of("…", FOREST, GRASS));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("BunchGrass");
    }
    create() {
      return new BunchGrass();
    }
    tag() {
      return "Grasses";
    }
  })();
}

class BeachGrass extends Terrain {
  constructor() {
    super(
      "Beach Grass",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of("…", DARKKHAKI, SAND),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("BeachGrass");
    }
    create() {
      return new BeachGrass();
    }
    tag() {
      return "Grasses";
    }
  })();
}

class SwampGrass extends Terrain {
  constructor() {
    super("Swamp Grass", TRAVERSABLE | PENETRABLE, NONE, Sym.of("…", GRASS, BUSHES));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("SwampGrass");
    }
    create() {
      return new SwampGrass();
    }
    tag() {
      return "Grasses";
    }
  })();
}

class Scrub extends Terrain {
  constructor() {
    super("Scrub", TRAVERSABLE | PENETRABLE, NONE, Sym.of("…", BUSHES, SAND));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Scrub");
    }
    create() {
      return new Scrub();
    }
    tag() {
      return "Grasses";
    }
  })();
}

class Weeds extends Terrain {
  constructor() {
    super("Weeds", TRAVERSABLE | PENETRABLE, NONE, Sym.of("…", GRASS, TAN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Weeds");
    }
    create() {
      return new Weeds();
    }
    tag() {
      return "Grasses";
    }
  })();
}

/** Bushes — traversable outdoor ground cover */
class Bushes extends Terrain {
  constructor() {
    super("Bushes", PENETRABLE | TRAVERSABLE, NONE, Sym.of("\u2003", NONE, BUSHES));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Bushes");
    }
    create() {
      return new Bushes();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Throne — directional seat; only enterable from the facing direction
 * and only exitable in the facing direction.
 */
export class Throne extends Terrain {
  direction: Direction;
  constructor(direction: Direction) {
    if (direction !== NORTH && direction !== SOUTH) {
      throw new Error("Throne must be north/south");
    }
    super(
      "Throne",
      TRAVERSABLE | PENETRABLE,
      NONE,
      Sym.of(
        direction === NORTH ? "◛" : "◚",
        SILVER,
        BLACK,
        BUILDING_WALL,
        BUILDING_FLOOR,
      ),
    );
    this.direction = direction;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    const required = this.direction === NORTH ? SOUTH : NORTH;
    if (dir !== required) event.cancel();
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir !== this.direction) event.cancel();
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([dir]: string[]) {
      return new Throne(directionByName(dir) ?? NORTH);
    }
    store(t: Throne) {
      return `Throne|${t.direction.name}`;
    }
    example() {
      return new Throne(NORTH);
    }
    template() {
      return "Throne|{direction}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * WishingWell — drop a GoldCoin to get healed. Wraps underlying terrain.
 */
export class WishingWell extends Terrain {
  terrain: Terrain;
  #state: State;
  constructor(terrain: Terrain, state: State) {
    super(
      "Well",
      PENETRABLE,
      NONE,
      Sym.of("Ф", OCEAN, terrain.symbol.getBackground(false)),
    );
    this.terrain = terrain;
    this.#state = state;
  }
  onDrop(event: GameEvent, cell: Cell, item: Item) {
    if (!(item instanceof Grenade)) {
      event.cancel(item.getDefiniteNoun("{0} falls into the well"), cell);
    }
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    const sel = player.bag.getSelected();
    if (sel instanceof GoldCoin) {
      player.bag.remove(sel);
      events.fireMessage("You toss a coin into the well.", cell);
      if (this.#state.isOn()) {
        _wishingWellHeal(player, cell, this, this.#state);
      } else {
        events.fireMessage("Nothing happens.", cell);
      }
    } else if (sel instanceof Rock) {
      events.fireMessage(
        "You toss a rock into the well. It's calming, isn't it?", cell
      );
    } else if (sel.name !== EMPTY_HANDED) {
      events.fireMessage("Try a coin...", cell);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new WishingWell(Registry.terrain(args[0]), stateFromString(args[1]) ?? ON);
    }
    store(w: WishingWell) {
      return `WishingWell|${this.esc(w.terrain)}|${w.#state.name}`;
    }
    example() {
      return new WishingWell(new Floor(), ON);
    }
    template() {
      return "WishingWell|{terrain}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

function _wishingWellHeal(player: Player, cell: Cell, well: WishingWell, state: State) {
  const currentValue = player.changeHealth(0);
  if (player.is(POISONED)) {
    player.remove(POISONED);
    events.fireMessage("You are no longer poisoned.", cell);
    TerrainUtils.toggleCellState(cell, well, state);
  } else if (player.is(WEAK)) {
    player.remove(WEAK);
    events.fireMessage("You no longer feel weak.", cell);
    TerrainUtils.toggleCellState(cell, well, state);
  } else if (currentValue < MAX_HEALTH) {
    const newValue = player.changeHealth(currentValue - MAX_HEALTH);
    events.fireMessage(`You're healed ${newValue - currentValue} points.`, cell);
    TerrainUtils.toggleCellState(cell, well, state);
  } else {
    events.fireMessage("Nothing happens.", cell);
  }
}

/**
 * Teleporter — transports the player to a specific board/x/y on entry.
 * Animates through RED→SALMON→ORANGE→WHITE cycling (mirrors Java Animated impl).
 */
export class Teleporter extends Terrain implements Animated {
  static SYMBOLS = [
    Sym.of("∞", RED, null, RED, BUILDING_FLOOR),
    Sym.of("∞", SALMON, null, SALMON, BUILDING_FLOOR),
    Sym.of("∞", ORANGE, null, ORANGE, BUILDING_FLOOR),
    Sym.of("∞", WHITE, null, WHITE, BUILDING_FLOOR),
  ];
  boardID: string;
  x: number;
  y: number;
  constructor(boardID: string, x: number, y: number) {
    super("Teleporter", TRAVERSABLE | PENETRABLE, NONE, Teleporter.SYMBOLS[0]);
    this.boardID = boardID;
    this.x = x;
    this.y = y;
  }
  randomSeed(): boolean {
    return true;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.game.teleport(event, dir, this.boardID, this.x, this.y);
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    cell.animTerrainSymbol = Teleporter.SYMBOLS[frame % 4];
    cell.board.notifyCellChange(cell);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([boardID, x, y]: string[]) {
      return new Teleporter(boardID, parseInt(x) || 0, parseInt(y) || 0);
    }
    store(t: Teleporter) {
      return `Teleporter|${t.boardID}|${t.x}|${t.y}`;
    }
    example() {
      return new Teleporter("board1", 0, 0);
    }
    template() {
      return "Teleporter|{boardID}|{x}|{y}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * ForceField — when ON, strips all carried items on entry.
 * When OFF, behaves like the wrapped terrain.
 */
export class ForceField extends Terrain implements Animated {
  static COLORS = [RED, YELLOW, ORANGE, VIOLET];
  direction: Direction;
  #state: State;
  terrain: Terrain;
  constructor(terrain: Terrain, direction: Direction, color: Color, state: State) {
    const bg = terrain.symbol.getBackground(false);
    const bgOut = terrain.symbol.getBackground(true);
    const sym = state.isOff()
      ? terrain.symbol
      : direction === NORTH
        ? Sym.of("|", RED, bg, RED, bgOut)
        : Sym.of("—", RED, bg, RED, bgOut);
    super("Force Field", TRAVERSABLE | PENETRABLE, color, sym);
    this.direction = direction;
    this.#state = state;
    this.terrain = terrain;
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color !== this.color) {
      return;
    }
    TerrainUtils.toggleCellState(cell, this, this.#state);
  }
  randomSeed() {
    return true;
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (!this.#state.isOn()) {
      return;
    }
    const outside = cell.board.outside;
    const bg = this.terrain.symbol.getBackground(outside);
    const char = this.direction === NORTH ? "|" : "\u2014";
    cell.animTerrainSymbol = Sym.of(char, ForceField.COLORS[frame % 4], bg);
    cell.board.notifyCellChange(cell);
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (!this.#state.isOn()) {
      return;
    }
    const bag = player.bag;
    const entries = [...bag.entries].filter(
      (e) => e.piece.name !== EMPTY_HANDED,
    );
    if (entries.length === 0) {
      return;
    }
    const prevCell = cell.getAdjacentCell(dir.reverse);
    for (const entry of entries) {
      const count = entry.count;
      for (let j = 0; j < count; j++) {
        prevCell?.addItem(entry.piece);
        bag.remove(entry.piece);
      }
    }
    events.fireModalMessage(
      "All your stuff gets flung from your body. It's kind of embarrassing.",
    );
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (this.#state.isOn()) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      if (args.length >= 4) {
        return new ForceField(
          Registry.terrain(args[0]),
          directionByName(args[1]) ?? DIR_NONE,
          colorByName(args[2]) ?? NONE,
          stateFromString(args[3]) ?? ON,
        );
      }
      return new ForceField(
        Registry.terrain("Floor"),
        directionByName(args[0]) ?? DIR_NONE,
        colorByName(args[1]) ?? NONE,
        stateFromString(args[2]) ?? ON,
      );
    }
    store(ff: ForceField) {
      return `ForceField|${this.esc(ff.terrain)}|${ff.direction.name}|${ff.color.name}|${ff.#state.name}`;
    }
    example() {
      return new ForceField(new Floor(), DIR_NONE, NONE, OFF);
    }
    template() {
      return "ForceField|{terrain?}|{direction}|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Reflector — ETHEREAL, deflects in-flight items.
 * Direction: north=|, northeast=/, east=—, southeast=\
 */
export class Reflector extends Terrain {
  reflectorDir: Direction;
  constructor(direction: Direction, color: Color) {
    if (
      direction !== NORTH &&
      direction !== NORTHEAST &&
      direction !== EAST &&
      direction !== SOUTHEAST
    ) {
      throw new Error("Reflector takes N/NE/E/SE directions only");
    }
    const entity =
      direction === NORTH
        ? "|"
        : direction === NORTHEAST
          ? "/"
          : direction === EAST
            ? "\u2014"
            : "\\";
    super(
      (color !== NONE ? color.name + " " : "") + "Reflector",
      ETHEREAL | AMMO_ENLIVENER,
      color,
      Sym.of(entity, color, SILVER),
    );
    this.reflectorDir = direction;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    this.#rotate(cell);
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color === this.color) {
      this.#rotate(cell);
    }
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    const reflected = _reflectedDir(this.reflectorDir, flier.direction);
    flier.direction = reflected;
    flier.originator = this;
  }
  #rotate(cell: Cell) {
    const dirNames = ["north", "northeast", "east", "southeast"];
    const curName = this.reflectorDir.name;
    const idx = dirNames.indexOf(curName);
    const nextName = dirNames[(idx + 1) % dirNames.length];
    const key = Registry.serialize(this);
    const parts = key.split("|");
    const colorPart = parts[2] ?? parts[1];
    cell.setTerrain(Registry.terrain(`Reflector|${nextName}|${colorPart}`));
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([dir, color]: string[]) {
      return new Reflector(
        directionByName(dir) ?? EAST,
        colorByName(color) ?? NONE,
      );
    }
    store(r: Reflector) {
      return `Reflector|${r.reflectorDir.name}|${r.color.name}`;
    }
    example() {
      return new Reflector(NORTH, RED);
    }
    template() {
      return "Reflector|{direction}|{color}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

const map: Record<string, Record<string, string>> = {
  north: {
    southeast: "southwest",
    southwest: "southeast",
    northeast: "northwest",
    northwest: "northeast",
  },
  northeast: { south: "west", west: "south", north: "east", east: "north" },
  east: {
    southeast: "northeast",
    northeast: "southeast",
    southwest: "northwest",
    northwest: "southwest",
  },
  southeast: { east: "south", south: "east", north: "west", west: "north" },
};

function _reflectedDir(mirror: Direction, incoming: Direction) {
  const row = map[mirror.name];
  if (!row || !row[incoming.name]) {
    return incoming.reverse;
  }
  return directionByName(row[incoming.name]) ?? incoming;
}

/**
 * BeeHive — impassable, spawns KillerBees on color event.
 */
export class BeeHive extends Terrain {
  #hiveState: State;
  hiveColor: Color;
  constructor(color: Color, state: State) {
    super("Bee Hive", 0, color, Sym.of("⌂", DARKGOLDENROD, GOLD));
    this.#hiveState = state;
    this.hiveColor = color;
  }
  canExit(agent: Agent, cell: Cell, dir: Direction) {
    return agent.name === "Killer Bee";
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    _annoyHive(cell, this.hiveColor, this.#hiveState);
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    event.cancel();
    _annoyHive(cell, this.hiveColor, this.#hiveState);
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color !== this.hiveColor) return;
    if (cell.agent == null) {
      cell.setAgent(Registry.agent(`KillerBee|${this.hiveColor.name}`));
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state]: string[]) {
      return new BeeHive(
        colorByName(color) ?? NONE,
        stateFromString(state) ?? OFF,
      );
    }
    store(bh: BeeHive) {
      return `BeeHive|${bh.hiveColor.name}|${bh.#hiveState.name}`;
    }
    example() {
      return new BeeHive(NONE, OFF);
    }
    template() {
      return "BeeHive|{color}";
    }
    tag() {
      return "Agents";
    }
  })();
}

function _annoyHive(cell: Cell, color: Color, state: State) {
  if (state.isOn()) {
    return;
  }
  try {
    const bee = Registry.agent(`KillerBee|${color.name}`);
    const adj = cell.getAdjacentCells(bee);
    for (const c of adj) {
      if (c.agent == null) {
        c.setAgent(bee);
      }
    }
    TerrainUtils.toggleCellState(cell, cell.terrain as Terrain, state);
  } catch (_) {
    /* ignore */
  }
}

/**
 * FarthapodNest — impassable, spawns Farthapods on color event.
 */
export class FarthapodNest extends Terrain {
  nestColor: Color;
  constructor(color: Color) {
    super(
      "Farthapod Nest",
      0,
      color,
      Sym.of("≡", LIGHTSTEELBLUE, BLACK, DARKSLATEBLUE, GRASS),
    );
    this.nestColor = color;
  }
  canExit(agent: Agent, cell: Cell, dir: Direction) {
    return agent.name === "Farthapod";
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color !== this.nestColor) return;
    if (cell.agent == null) {
      cell.setAgent(Registry.agent(`Farthapod|${this.nestColor.name}`));
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color]: string[]) {
      return new FarthapodNest(colorByName(color) ?? NONE);
    }
    store(fn: FarthapodNest) {
      return `FarthapodNest|${fn.nestColor.name}`;
    }
    example() {
      return new FarthapodNest(NONE);
    }
    template() {
      return "FarthapodNest|{color}";
    }
    tag() {
      return "Agents";
    }
  })();
}

/**
 * Turnstile — one-way passage (east or west only).
 * A color event reverses the direction.
 */
export class Turnstile extends Terrain implements Animated {
  #coloredSymbol;
  direction: Direction;
  constructor(direction: Direction, color: Color) {
    if (direction !== WEST && direction !== EAST) {
      throw new Error("Turnstiles must specify west/east travel");
    }
    const entity = direction === WEST ? "«" : "»";
    super(
      "Turnstile",
      0,
      color,
      Sym.of(entity, WHITE, BLACK, BLACK, BUILDING_FLOOR),
    );
    this.direction = direction;
    this.#coloredSymbol = Sym.of(entity, color, null, color, BUILDING_FLOOR);
  }
  randomSeed() {
    return true;
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (frame % 30 === 0) {
      cell.animTerrainSymbol = Sym.of(
        this.direction === WEST ? "«" : "»",
        WHITE,
        BLACK,
        BLACK,
        BUILDING_FLOOR,
      );
      cell.board.notifyCellChange(cell);
    } else if (frame % 15 === 0) {
      cell.animTerrainSymbol = this.#coloredSymbol;
      cell.board.notifyCellChange(cell);
    }
  }
  canEnter(agent: Agent, cell: Cell, dir: Direction) {
    return dir === this.direction;
  }
  canExit(agent: Agent, cell: Cell, dir: Direction) {
    return dir === this.direction;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir !== this.direction) {
      event.cancel();
    }
  }
  onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (dir !== this.direction) {
      event.cancel();
    }
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (dir !== this.direction) {
      event.cancel();
    }
  }
  onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (dir !== this.direction) {
      event.cancel();
    }
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    if (this.direction !== flier.direction) {
      event.cancel();
    }
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color === this.color) {
      const newDirName = this.direction === EAST ? "west" : "east";
      cell.setTerrain(Registry.terrain(`Turnstile|${newDirName}|${this.color.name}`));
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([dir, color]: string[]) {
      return new Turnstile(directionByName(dir) ?? EAST, colorByName(color) ?? NONE);
    }
    store(t: Turnstile) {
      return `Turnstile|${t.direction.name}|${t.color.name}`;
    }
    example() {
      return new Turnstile(EAST, NONE);
    }
    template() {
      return "Turnstile|{direction}|{color}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Shooter — fires an ammo item in a direction (animated via board's turn cycle).
 */
export class Shooter extends Terrain implements Animated {
  shootAmmo: Item;
  shootDir: Direction;
  #shootState: State;
  constructor(ammo: Item, direction: Direction, color: Color, state: State) {
    super(
      "Shooter",
      0,
      color,
      Sym.of("ж", SALMON, NEARBLACK, SALMON, BUILDING_WALL),
    );
    this.shootAmmo = ammo;
    this.shootDir = direction;
    this.#shootState = state;
  }
  onColorEvent(event: GameEvent, color: Color, cell: Cell) {
    if (color !== this.color) {
      return;
    }
    TerrainUtils.toggleCellState(cell, this, this.#shootState);
  }
  randomSeed(): boolean {
    return true;
  }
  onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
    // Allow only the shooter's own ammo to leave
    if (flier.item !== this.shootAmmo) {
      event.cancel();
    }
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (frame % 5 !== 0 || !this.#shootState.isOn()) {
      return;
    }
    const dir =
      this.shootDir === DIR_NONE ? getRandomDirection() : this.shootDir;
    game.shoot(event, cell, this, this.shootAmmo, dir);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      const ammo = Registry.item(args[0]);
      if (args.length >= 4) {
        return new Shooter(
          ammo,
          directionByName(args[1]) ?? DIR_NONE,
          colorByName(args[2]) ?? NONE,
          stateFromString(args[3]) ?? ON,
        );
      }
      return new Shooter(
        ammo,
        DIR_NONE,
        colorByName(args[1]) ?? NONE,
        stateFromString(args[2]) ?? ON,
      );
    }
    store(s: Shooter) {
      return `Shooter|${this.esc(s.shootAmmo)}|${s.shootDir.name}|${s.color.name}|${s.#shootState.name}`;
    }
    example() {
      return new Shooter(Registry.item("Bullet"), NORTH, NONE, ON);
    }
    template() {
      return "Shooter|{ammo}|{direction}|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

export class FishPool extends Terrain implements Animated {
  #terrain;
  #state;
  constructor(terrain: Terrain, state: State) {
    const bg = terrain.symbol.getBackground(false);
    const bgOut = terrain.symbol.getBackground(true);
    const name = state.isOn() ? `${terrain.name} with Fish` : terrain.name;
    super(
      name,
      AQUATIC,
      NONE,
      state.isOn() ? Sym.of("α", SURF, bg, SURF, bgOut) : terrain.symbol,
    );
    this.#terrain = terrain;
    this.#state = state;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    const sel = player.bag.getSelected();
    if (this.#state.isOn() && sel instanceof FishingPole) {
      player.bag.add(Registry.item("Fish"));
      events.fireMessage("You caught a fish!", cell);
      if (Math.random() * 100 < 80) {
        this.#fishSwims(cell);
      } else {
        cell.setTerrain(this.#terrain);
      }
    }
  }
  #fishSwims(cell: Cell) {
    const board = cell.board;
    const candidates = [];
    for (const dir of [
      NORTH,
      SOUTH,
      EAST,
      WEST,
      NORTHEAST,
      NORTHWEST,
      SOUTHEAST,
      SOUTHWEST,
    ]) {
      const adj = board.getAdjacentCell(cell.x, cell.y, dir);
      if (adj && adj.terrain === this.#terrain) {
        candidates.push(adj);
      }
    }
    const target = candidates.length
      ? candidates[Math.floor(Math.random() * candidates.length)]
      : null;
    if (target) {
      // Place an OFF fish pool on the target — it will later turn ON randomly
      const offPool = TerrainUtils.getTerrainOtherState(this, this.#state);
      target.setTerrain(offPool);
      // Revert this cell to plain base terrain
      cell.setTerrain(this.#terrain);
    }
  }
  randomSeed(): boolean {
    return true;
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (frame % 8 !== 0) {
      return;
    }
    const isOn = this.#state.isOn();
    const chanceToChange = isOn ? 40 : 20;
    if (Math.random() * 100 >= chanceToChange) {
      return;
    }
    if (isOn) {
      this.#fishSwims(cell);
    } else {
      TerrainUtils.toggleCellState(cell, this, this.#state);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new FishPool(Registry.terrain(args[0]), stateFromString(args[1]) ?? ON);
    }
    store(fp: FishPool) {
      return `FishPool|${this.esc(fp.#terrain)}|${fp.#state.name}`;
    }
    example() {
      return new FishPool(new Floor(), ON);
    }
    template() {
      return "FishPool|{terrain}|{state}";
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * EuclideanEngine — accepts a matching-color EuclideanShard to power up.
 */
export class EuclideanEngine extends Terrain implements Animated {
  #engineState: State;
  #symbols: Sym[] = [];
  constructor(color: Color, state: State) {
    super(
      `${color.name} Euclidean Engine`,
      0,
      color,
      Sym.of("◊", color, null, color, BUILDING_FLOOR),
    );
    this.#engineState = state;
    if (state.isOn()) {
      // can’t be static because the engine has a specific color
      this.#symbols = [
        Sym.of("◊", color, null, color, BUILDING_FLOOR),
        Sym.of("◆", color, null, color, BUILDING_FLOOR),
        Sym.of("◊", BLACK, color, BUILDING_FLOOR, color),
      ];
    } else {
      this.#symbols = [Sym.of("◊", color, null, color, BUILDING_FLOOR)];
    }
  }
  randomSeed() {
    return true;
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    if (this.#engineState.isOn()) {
      cell.animTerrainSymbol = this.#symbols[frame % 3];
      cell.board.notifyCellChange(cell);
    }
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    if (this.#engineState.isOn()) {
      return;
    }
    const sel = player.bag.getSelected();
    if (sel instanceof EuclideanShard) {
      if (sel.color === this.color) {
        player.bag.remove(sel);
        TerrainUtils.toggleCellState(cell, this, this.#engineState);
        this.#checkAllEnginesOn(event);
      } else {
        events.fireMessage(
          "The shard is not the right color for this engine", cell
        );
      }
    } else if (sel.name !== EMPTY_HANDED) {
      events.fireMessage(
        `It isn't working, and the ${sel.name} doesn't seem to help`, cell
      );
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([color, state]: string[]) {
      return new EuclideanEngine(
        colorByName(color) ?? NONE,
        stateFromString(state) ?? OFF,
      );
    }
    store(ee: EuclideanEngine) {
      return `EuclideanEngine|${ee.color.name}|${ee.#engineState.name}`;
    }
    example() {
      return new EuclideanEngine(NONE, OFF);
    }
    template() {
      return "EuclideanEngine|{color}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
  #checkAllEnginesOn(event: GameEvent) {
    let count = 0;
    event.board.visit((cell: Cell) => {
      if (
        cell.terrain instanceof EuclideanEngine &&
        cell.terrain.#engineState.isOn()
      ) {
        count++;
      }
      return true;
    });
    if (count === 4) {
      event.board.visit((cell) => {
        if (
          cell.terrain instanceof EuclideanTransporter &&
          !cell.terrain.etState.isOn()
        ) {
          events.fireMessage(
            "As the last engine starts, the Euclidean Transporter also powers up.", cell
          );
          TerrainUtils.toggleCellState(cell, cell.terrain, cell.terrain.etState);
        }
        return true;
      });
    }
  }
}

/**
 * EuclideanTransporter — teleports to another board when ON.
 */
export class EuclideanTransporter extends Terrain implements Animated {
  static SYMBOLS = [
    Sym.of("⨁", RED, null, RED, BUILDING_FLOOR),
    Sym.of("⨁", BLUE, null, BLUE, BUILDING_FLOOR),
    Sym.of("⨂", RED, null, RED, BUILDING_FLOOR),
    Sym.of("⨂", BLUE, null, BLUE, BUILDING_FLOOR),
    Sym.of("⨴", RED, null, RED, BUILDING_FLOOR),
    Sym.of("⨴", BLUE, null, BLUE, BUILDING_FLOOR),
    Sym.of("⨵", RED, null, RED, BUILDING_FLOOR),
    Sym.of("⨵", BLUE, null, BLUE, BUILDING_FLOOR),
  ];
  #etBoardID: string;
  #etX: number;
  #etY: number;
  etState: State;
  constructor(boardID: string, x: number, y: number, state: State) {
    super(
      "Euclidean Transporter",
      TRAVERSABLE | PENETRABLE,
      NONE,
      EuclideanTransporter.SYMBOLS[0],
    );
    this.#etBoardID = boardID;
    this.#etX = x;
    this.#etY = y;
    this.etState = state;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    if (this.etState.isOn()) {
      event.game.teleport(event, dir, this.#etBoardID, this.#etX, this.#etY);
    } else {
      events.fireMessage("You're standing on a complex device", cell);
    }
  }
  onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
    if (this.etState.isOn()) {
      event.cancel();
    }
  }
  randomSeed() {
    return true;
  }
  onFrame(event: GameEvent, cell: Cell, frame: number) {
    const syms = EuclideanTransporter.SYMBOLS;
    // note that slowing down the animation with a mod function means that some symbols are skipped
    if (this.etState.isOn()) {
      cell.animTerrainSymbol = syms[frame % syms.length];
      cell.board.notifyCellChange(cell);
    }
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create(args: string[]) {
      return new EuclideanTransporter(
        args[0],
        parseInt(args[1]) || 0,
        parseInt(args[2]) || 0,
        stateFromString(args[3]) ?? OFF,
      );
    }
    store(et: EuclideanTransporter) {
      return `EuclideanTransporter|${et.#etBoardID}|${et.#etX}|${et.#etY}|${et.etState.name}`;
    }
    example() {
      return new EuclideanTransporter("board1", 0, 0, OFF);
    }
    template() {
      return "EuclideanTransporter|{boardID}|{x}|{y}|{state}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * Exchanger — swaps the player's selected item with the top item on the adjacent cell.
 */
export class Exchanger extends Terrain {
  constructor() {
    super(
      "Exchanger",
      0,
      NONE,
      Sym.of("÷", LIGHTSLATEGRAY, NEARBLACK, DARKSLATEGRAY, BUILDING_WALL),
    );
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    if (dir.isDiagonal()) {
      return;
    }
    const otherCell = cell.getAdjacentCell(dir);
    if (!otherCell) {
      return;
    }
    const otherItem = otherCell.topItem;
    const sel = player.bag.getSelected();
    if (sel instanceof Rock) {
      events.fireMessage("The exchanger does not accept rocks", cell);
    } else if (sel && sel.name !== EMPTY_HANDED && otherItem) {
      otherCell.removeItem(otherItem);
      otherCell.addItem(sel);
      player.bag.exchange(otherItem);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Exchanger");
    }
    create() {
      return new Exchanger();
    }
    tag() {
      return "Room Features";
    }
  })();
}

/**
 * VendingMachine — sells items from the adjacent cell for GoldCoins.
 * Format: VendingMachine|{cost}
 */
export class VendingMachine extends Terrain {
  cost: number;
  constructor(cost: number) {
    super(
      `Vending Machine (${cost} coins)`,
      0,
      NONE,
      Sym.of("\u00F7", GOLD, NEARBLACK, GOLDENROD, BUILDING_WALL),
    );
    this.cost = cost;
  }
  onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
    event.cancel();
    if (dir.isDiagonal()) {
      return;
    }
    const otherCell = cell.getAdjacentCell(dir);
    if (!otherCell) {
      return;
    }
    const otherItem = otherCell.topItem;
    if (!otherItem) {
      events.fireMessage("The vending machine is empty", cell);
      return;
    }
    const goldCoin = player.bag.find((item) => item.name === "Gold Coin") as GoldCoin;
    const goldCount = goldCoin ? player.bag.getCount(goldCoin) : 0;
    if (goldCount < this.cost) {
      events.fireMessage(`You need ${this.cost} gold coins`, cell);
      return;
    }
    for (let i = 0; i < this.cost; i++) {
      player.bag.remove(goldCoin);
    }
    otherCell.removeItem(otherItem);
    player.bag.add(otherItem);
    events.fireMessage(`You purchase the ${otherItem.name}`, cell);
  }
  static SERIALIZER = new (class extends BaseSerializer {
    create([cost]: string[]) {
      return new VendingMachine(parseInt(cost) || 1);
    }
    store(vm: VendingMachine) {
      return `VendingMachine|${vm.cost}`;
    }
    example() {
      return new VendingMachine(1);
    }
    template() {
      return "VendingMachine|{cost}";
    }
    tag() {
      return "Room Features";
    }
  })();
}

export function registerBasicTerrain() {
  Registry.register("BeachGrass", BeachGrass.SERIALIZER);
  Registry.register("BeeHive", BeeHive.SERIALIZER);
  Registry.register("Bridge", Bridge.SERIALIZER);
  Registry.register("BunchGrass", BunchGrass.SERIALIZER);
  Registry.register("Bushes", Bushes.SERIALIZER);
  Registry.register("ChalkedFloor", ChalkedFloor.SERIALIZER);
  Registry.register("Cloud", Cloud.SERIALIZER);
  Registry.register("Crevasse", Crevasse.SERIALIZER);
  Registry.register("Dirt", Dirt.SERIALIZER);
  Registry.register("EuclideanEngine", EuclideanEngine.SERIALIZER);
  Registry.register("EuclideanTransporter", EuclideanTransporter.SERIALIZER);
  Registry.register("Exchanger", Exchanger.SERIALIZER);
  Registry.register("FarthapodNest", FarthapodNest.SERIALIZER);
  Registry.register("Fence", Fence.SERIALIZER);
  Registry.register("Field", Field.SERIALIZER);
  Registry.register("FishPool", FishPool.SERIALIZER);
  Registry.register("Floor", Floor.SERIALIZER);
  Registry.register("Flowers", Flowers.SERIALIZER);
  Registry.register("ForceField", ForceField.SERIALIZER);
  Registry.register("Forest", Forest.SERIALIZER);
  Registry.register("Grass", Grass.SERIALIZER);
  Registry.register("Haystack", Haystack.SERIALIZER);
  Registry.register("HighRocks", HighRocks.SERIALIZER);
  Registry.register("ImpassableCliffs", ImpassableCliffs.SERIALIZER);
  Registry.register("LowRocks", LowRocks.SERIALIZER);
  Registry.register("Pier", Pier.SERIALIZER);
  Registry.register("PyramidWall", PyramidWall.SERIALIZER);
  Registry.register("Reflector", Reflector.SERIALIZER);
  Registry.register("Sand", Sand.SERIALIZER);
  Registry.register("Scrub", Scrub.SERIALIZER);
  Registry.register("Shooter", Shooter.SERIALIZER);
  Registry.register("Sky", Sky.SERIALIZER);
  Registry.register("SwampGrass", SwampGrass.SERIALIZER);
  Registry.register("TallGrass", TallGrass.SERIALIZER);
  Registry.register("Teleporter", Teleporter.SERIALIZER);
  Registry.register("Throne", Throne.SERIALIZER);
  Registry.register("TrashPile", TrashPile.SERIALIZER);
  Registry.register("Turnstile", Turnstile.SERIALIZER);
  Registry.register("VendingMachine", VendingMachine.SERIALIZER);
  Registry.register("Wall", Wall.SERIALIZER);
  Registry.register("Weeds", Weeds.SERIALIZER);
  Registry.register("WishingWell", WishingWell.SERIALIZER);
  Registry.register("WoodPiling", WoodPiling.SERIALIZER);
}