pieces/terrain/water.js

import { BlueRing } from "../../pieces/items/items.js";
import { Terrain } from "../../core/terrain.js";
import { Registry } from "../../core/registry.js";
import { TypeOnlySerializer } from "../../core/serializer.js";
import { events } from "../../core/events.js";
import {
  TRAVERSABLE,
  PENETRABLE,
  ETHEREAL,
  AQUATIC,
  LAVITIC,
  WATER_RESISTANT,
} from "../../core/flags.js";
import {
  DARKKHAKI,
  OCEAN,
  SIENNA,
  SURF,
  MUD,
  BUSHES,
  LAVA,
  LIGHTPINK,
} from "../../core/color.js";
import { Symbol } from "../../core/symbol.js";

// ── Water ─────────────────────────────────────────────────────────────────────

/**
 * Deep water — only AQUATIC agents (or player with BlueRing/WATER_RESISTANT) can enter.
 */
class Water extends Terrain {
  constructor() {
    super("Water", AQUATIC, Symbol.of("\u2003", null, OCEAN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Water");
    }
    create(_args) {
      return new Water();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Ocean — same as Water but cancels player entry unless they have
 * the blue ring or WATER_RESISTANT flag. (For some reason I thought
 * it was important that it be the blue ring, not just WATER_RESISTANT,
 * although the blue ring gives you the WATER_RESISTANT flag).
 */
class Ocean extends Terrain {
  constructor() {
    super("Ocean", AQUATIC, Symbol.of("\u2003", null, OCEAN));
  }
  onEnter(event, player, cell, _dir) {
    if (
      !player.is(WATER_RESISTANT) &&
      !player.bag.find((i) => i instanceof BlueRing)
    ) {
      events.fireMessage(cell, "You cannot enter the deep water");
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Ocean");
    }
    create(_args) {
      return new Ocean();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** ShallowWater — traversable by anyone; just wet */
class ShallowWater extends Terrain {
  constructor() {
    super(
      "Shallow Water",
      TRAVERSABLE | PENETRABLE,
      Symbol.of("\u2003", null, SURF),
    );
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("ShallowWater");
    }
    create(_args) {
      return new ShallowWater();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** Surf — identical appearance to ShallowWater (beach wave edge) */
class Surf extends Terrain {
  constructor() {
    super("Surf", TRAVERSABLE | PENETRABLE, Symbol.of("\u2003", null, SURF));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Surf");
    }
    create(_args) {
      return new Surf();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Mud — ETHEREAL (blocks items), traversable but sticky.
 * On exit there's a 70% chance you are stuck (move is cancelled).
 */
class Mud extends Terrain {
  constructor() {
    super(
      "Mud",
      TRAVERSABLE | PENETRABLE | ETHEREAL,
      Symbol.of("\u2003", null, MUD),
    );
  }
  onExit(event, player, cell, _dir) {
    if (player.getCurrentCell?.() === cell || cell.containsPlayer()) {
      if (Math.random() < 0.7) {
        events.fireMessage(cell, "You are stuck in the mud!");
        event.cancel();
      }
    }
  }
  onAgentExit(event, _agent, cell, _dir) {
    if (Math.random() < 0.3) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Mud");
    }
    create(_args) {
      return new Mud();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * ShallowSwamp — ETHEREAL, traversable, sticky (same as Mud).
 * 70% stuck on exit (player), 30% stuck (agent).
 */
class ShallowSwamp extends Terrain {
  constructor() {
    super(
      "Shallow Swamp",
      TRAVERSABLE | PENETRABLE | ETHEREAL,
      Symbol.of("…", BUSHES, SURF),
    );
  }
  onExit(event, _player, cell, _dir) {
    if (Math.random() < 0.7) {
      events.fireMessage(cell, "You are stuck in the swamp!");
      event.cancel();
    }
  }
  onAgentExit(event, _agent, _cell, _dir) {
    if (Math.random() < 0.3) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("ShallowSwamp");
    }
    create(_args) {
      return new ShallowSwamp();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Swamp — ETHEREAL, traversable, deep mucky water.
 * Same sticky mechanic as ShallowSwamp.
 */
class Swamp extends Terrain {
  constructor() {
    super(
      "Swamp",
      TRAVERSABLE | PENETRABLE | ETHEREAL,
      Symbol.of("…", BUSHES, OCEAN),
    );
  }
  onExit(event, _player, cell, _dir) {
    if (Math.random() < 0.7) {
      events.fireMessage(cell, "You are stuck in the swamp!");
      event.cancel();
    }
  }
  onAgentExit(event, _agent, _cell, _dir) {
    if (Math.random() < 0.3) {
      event.cancel();
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Swamp");
    }
    create(_args) {
      return new Swamp();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/** Lava — LAVITIC: only lava-adapted agents can enter */
class Lava extends Terrain {
  constructor() {
    super("Lava", LAVITIC, Symbol.of("\u2003", null, LAVA));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Lava");
    }
    create(_args) {
      return new Lava();
    }
    tag() {
      return "Terrain";
    }
  })();
}

/**
 * BubblingLava — animated lava that cycles through 8 symbols, then moves
 * to an adjacent Lava cell (mirroring the Java implementation).
 *
 * Java: cycles every 4 ticks, full period = 36 (8 symbols + 1 move tick).
 * At t=8 (frame%36===32) it swaps cells with an adjacent lava cell twice
 * to "flow" in a random direction.
 */
class BubblingLava extends Terrain {
  static SYMBOLS = [
    Symbol.of(".", LIGHTPINK, LAVA),
    Symbol.of(":", LIGHTPINK, LAVA),
    Symbol.of("'", LIGHTPINK, LAVA),
    Symbol.of("ν", LIGHTPINK, LAVA),
    Symbol.of("∴", LIGHTPINK, LAVA),
    Symbol.of("⋅", LIGHTPINK, LAVA),
    Symbol.of(".", LIGHTPINK, LAVA),
    Symbol.of("\u2003", LIGHTPINK, LAVA),
  ];
  constructor() {
    super("Bubbling Lava", LAVITIC, BubblingLava.SYMBOLS[0]);
  }
  /** @param {GameEvent} event @param {Cell} cell @param {number} frame */
  onFrame(event, cell, frame) {
    if (frame % 4 !== 0) return;
    const t = ((frame % 36) / 4) | 0; // 0..8
    if (t < 8) {
      cell._animTerrainSymbol = BubblingLava.SYMBOLS[t];
      cell.board._notifyCellChange(cell);
    } else {
      // t === 8: flow — find a lava cell adjacent to an adjacent lava cell (two hops)
      const lava = Registry.get("Lava");
      let dest = this._findAdjacentLava(cell);
      dest = this._findAdjacentLava(dest);
      if (dest) {
        cell._animTerrainSymbol = null;
        cell.setTerrain(lava);
        dest.setTerrain(this);
      }
    }
  }
  /** @private */
  _findAdjacentLava(cell) {
    if (!cell) return null;
    const dirs = [
      [1, 0],
      [-1, 0],
      [0, 1],
      [0, -1],
    ];
    const candidates = [];
    for (const [dx, dy] of dirs) {
      const adj = cell.board.getCellAt(cell.x + dx, cell.y + dy);
      if (adj && adj.terrain && adj.terrain.name === "Lava")
        candidates.push(adj);
    }
    if (candidates.length === 0) return null;
    return candidates[Math.floor(Math.random() * candidates.length)];
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("BubblingLava");
    }
    create(_args) {
      return new BubblingLava();
    }
    tag() {
      return "Terrain";
    }
  })();
}

/**
 * Waterfall — impassable animated waterfall (no TRAVERSABLE).
 * Renders as static for now.
 */
class Waterfall extends Terrain {
  constructor() {
    super("Waterfall", Symbol.of("≈", SURF, OCEAN));
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Waterfall");
    }
    create(_args) {
      return new Waterfall();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Raft — a water-traversal terrain. When an agent moves off a raft onto
 * Water or Ocean, the raft moves with them (swapping cells). Moving onto
 * Waterfall or shallow water is blocked with a message.
 */
class Raft extends Terrain {
  constructor() {
    super(
      "Raft",
      TRAVERSABLE | PENETRABLE,
      Symbol.of("\u2263", SIENNA, DARKKHAKI),
    );
  }
  _moveRaft(event, cell, dir) {
    const next = cell.getAdjacentCell(dir);
    if (!next) return;
    const t = next.terrain;
    if (t instanceof Water) {
      next.setTerrain(this);
      cell.setTerrain(Registry.get("Water"));
    } else if (t instanceof Ocean) {
      next.setTerrain(this);
      cell.setTerrain(Registry.get("Ocean"));
    } else if (t instanceof Waterfall) {
      event.cancelWithMessage(cell, "The water is too turbulent for a raft");
    } else if (t instanceof ShallowWater || t instanceof Surf) {
      event.cancelWithMessage(cell, "It's too shallow for the raft here.");
    }
  }
  onExit(event, _player, cell, dir) {
    this._moveRaft(event, cell, dir);
  }
  onAgentExit(event, _agent, cell, dir) {
    this._moveRaft(event, cell, dir);
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Raft");
    }
    create(_args) {
      return new Raft();
    }
    tag() {
      return "Outside Terrain";
    }
  })();
}

/**
 * Fountain — impassable animated fountain.
 *
 * Java: 5 symbols, cycles every 3 ticks, full period = 15.
 * Uses randomSeed so multiple fountains animate independently.
 */
class Fountain extends Terrain {
  static SYMBOLS = [
    Symbol.of("·", SURF, OCEAN),
    Symbol.of("•", SURF, OCEAN),
    Symbol.of("o", SURF, OCEAN),
    Symbol.of("O", SURF, OCEAN),
    Symbol.of("©", SURF, OCEAN),
  ];
  constructor() {
    super("Fountain", Fountain.SYMBOLS[0]);
  }
  /** @param {GameEvent} event @param {Cell} cell @param {number} frame */
  onFrame(event, cell, frame) {
    if (frame % 3 === 0) {
      cell._animTerrainSymbol = Fountain.SYMBOLS[(frame % 15) / 3];
      cell.board._notifyCellChange(cell);
    }
  }
  static SERIALIZER = new (class extends TypeOnlySerializer {
    constructor() {
      super("Fountain");
    }
    create(_args) {
      return new Fountain();
    }
    tag() {
      return "Room Features";
    }
  })();
}

// ── Registry ──────────────────────────────────────────────────────────────────

export function registerWaterTerrain() {
  Registry.register("Water", Water.SERIALIZER);
  Registry.register("Ocean", Ocean.SERIALIZER);
  Registry.register("ShallowWater", ShallowWater.SERIALIZER);
  Registry.register("Surf", Surf.SERIALIZER);
  Registry.register("Mud", Mud.SERIALIZER);
  Registry.register("ShallowSwamp", ShallowSwamp.SERIALIZER);
  Registry.register("Swamp", Swamp.SERIALIZER);
  Registry.register("Lava", Lava.SERIALIZER);
  Registry.register("BubblingLava", BubblingLava.SERIALIZER);
  Registry.register("Waterfall", Waterfall.SERIALIZER);
  Registry.register("Raft", Raft.SERIALIZER);
  Registry.register("Fountain", Fountain.SERIALIZER);
}