import { BlueRing } from "../../pieces/items/items.ts";
import { Terrain } from "../../core/terrain.ts";
import { Registry } from "../../core/registry.ts";
import { TypeOnlySerializer } from "../../core/serializer.ts";
import { events } from "../../core/event-bus.ts";
import {
TRAVERSABLE,
PENETRABLE,
ETHEREAL,
AQUATIC,
LAVITIC,
WATER_RESISTANT,
} from "../../core/flags.ts";
import {
DARKKHAKI,
OCEAN,
SIENNA,
SURF,
MUD,
BUSHES,
LAVA,
LIGHTPINK,
WAVES,
NONE,
} from "../../core/color.ts";
import { Sym } from "../../core/sym.ts";
import { Item } from "../../core/item.ts";
import { Cell } from "../../core/cell.ts";
import { GameEvent } from "../../core/game-event.ts";
import { Direction } from "../../core/direction.ts";
import { Player } from "../../core/player.ts";
import { Agent } from "../../core/agent.ts";
import { Animated } from "../../core/animated.ts";
/**
* Deep water — only AQUATIC agents (or player with BlueRing/WATER_RESISTANT) can enter.
*/
class Water extends Terrain {
constructor() {
super("Water", AQUATIC, NONE, Sym.of("\u2003", NONE, OCEAN));
}
onDrop(event: GameEvent, cell: Cell, item: Item) {
event.cancel("It falls into the water.", cell);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Water");
}
create() {
return new Water();
}
tag() {
return "Outside Terrain";
}
})();
}
/**
* Ocean — deep water. Unlike Water, entry is always blocked for the
* player, even with the blue ring or WATER_RESISTANT: the cold, dark,
* and pressure of the deep ocean are too much regardless.
*/
class Ocean extends Terrain {
constructor() {
super("Ocean", AQUATIC, NONE, Sym.of("\u2003", NONE, OCEAN));
}
onEnter(event: GameEvent, player: Player, cell: Cell, _dir: Direction) {
event.cancel();
const item = player.bag.getSelected();
if (item instanceof BlueRing) {
events.fireMessage(
"You can breathe underwater with the ring on, but the cold, the dark and the pressure are too much to continue", cell
);
} else if (player.is(WATER_RESISTANT)) {
events.fireMessage(
"The cold, the dark, and the pressure are too much for you.", cell
);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Ocean");
}
create() {
return new Ocean();
}
tag() {
return "Outside Terrain";
}
})();
}
/** ShallowWater — traversable by anyone; just wet */
class ShallowWater extends Terrain {
constructor() {
super(
"Shallow Water",
TRAVERSABLE | PENETRABLE,
NONE,
Sym.of("\u2003", NONE, SURF),
);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("ShallowWater");
}
create() {
return new ShallowWater();
}
tag() {
return "Outside Terrain";
}
})();
}
/** Surf — identical appearance to ShallowWater (beach wave edge) */
class Surf extends Terrain {
constructor() {
super("Surf", TRAVERSABLE | PENETRABLE, NONE, Sym.of("\u2003", NONE, SURF));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Surf");
}
create() {
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,
NONE,
Sym.of("\u2003", NONE, MUD),
);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (event.board.getCurrentCell() === cell || cell.containsPlayer()) {
if (Math.random() < 0.7) {
events.fireMessage("You are stuck in the mud!", cell);
event.cancel();
}
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Mud");
}
create() {
return new Mud();
}
tag() {
return "Outside Terrain";
}
})();
}
/**
* ShallowSwamp — ETHEREAL, traversable, sticky, but easier to escape than
* deep Swamp: only 30% stuck on exit (vs. Swamp's 70%).
*/
class ShallowSwamp extends Terrain {
constructor() {
super(
"Shallow Swamp",
TRAVERSABLE | PENETRABLE | ETHEREAL,
NONE,
Sym.of("…", BUSHES, SURF),
);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (Math.random() < 0.3) {
events.fireMessage("You are stuck in the swamp!", cell);
event.cancel();
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("ShallowSwamp");
}
create() {
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,
NONE,
Sym.of("…", BUSHES, OCEAN),
);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (Math.random() < 0.7) {
events.fireMessage("You are stuck in the swamp!", cell);
event.cancel();
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Swamp");
}
create() {
return new Swamp();
}
tag() {
return "Outside Terrain";
}
})();
}
/** Lava — LAVITIC: only lava-adapted agents can enter */
class Lava extends Terrain {
constructor() {
super("Lava", LAVITIC, NONE, Sym.of("\u2003", NONE, LAVA));
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Lava");
}
create() {
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 implements Animated {
static SYMBOLS = [
Sym.of(".", LIGHTPINK, LAVA),
Sym.of(":", LIGHTPINK, LAVA),
Sym.of("'", LIGHTPINK, LAVA),
Sym.of("ν", LIGHTPINK, LAVA),
Sym.of("∴", LIGHTPINK, LAVA),
Sym.of("⋅", LIGHTPINK, LAVA),
Sym.of(".", LIGHTPINK, LAVA),
Sym.of("\u2003", LIGHTPINK, LAVA),
];
constructor() {
super("Bubbling Lava", LAVITIC, NONE, BubblingLava.SYMBOLS[0]);
}
randomSeed(): boolean {
return true;
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
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.terrain("Lava");
let dest = this.#findAdjacentLava(cell);
if (dest) {
dest = this.#findAdjacentLava(dest);
if (dest) {
cell.animTerrainSymbol = null;
cell.setTerrain(lava);
dest.setTerrain(this);
}
}
}
}
/** @private */
#findAdjacentLava(cell: Cell): Cell | 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() {
return new BubblingLava();
}
tag() {
return "Terrain";
}
})();
}
/**
* Waterfall — impassable animated waterfall. Unlike Water/Ocean this is not
* AQUATIC: flags are 0, so it blocks everyone, even aquatic agents and
* water-resistant players (a waterfall is too turbulent for anyone to cross).
*
* Java: 5 symbols, cycles every 3 ticks, full period = 15 (same cadence as
* Fountain). Uses randomSeed so multiple waterfalls animate independently.
*/
class Waterfall extends Terrain implements Animated {
static SYMBOLS = [
Sym.of("¨", SURF, OCEAN),
Sym.of("≈", SURF, OCEAN),
Sym.of("…", SURF, OCEAN)
];
constructor() {
super("Waterfall", 0, NONE, Waterfall.SYMBOLS[0]);
}
randomSeed(): boolean {
return true;
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
if (frame % 3 === 0) {
cell.animTerrainSymbol = Waterfall.SYMBOLS[(frame % 15) / 3];
cell.board.notifyCellChange(cell);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Waterfall");
}
create() {
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,
NONE,
Sym.of("\u2263", SIENNA, DARKKHAKI),
);
}
#moveRaft(event: GameEvent, cell: Cell, dir: Direction) {
const next = cell.getAdjacentCell(dir);
if (!next) return;
const t = next.terrain;
if (t instanceof Waterfall) {
event.cancel("The water is too turbulent for a raft", cell);
} else if (t instanceof ShallowWater || t instanceof Surf) {
event.cancel("It's too shallow for the raft here.", cell);
} else if (t && t.is(AQUATIC)) {
next.setTerrain(this);
cell.setTerrain(t);
}
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
this.#moveRaft(event, cell, dir);
}
onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
this.#moveRaft(event, cell, dir);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Raft");
}
create() {
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 implements Animated {
static SYMBOLS = [
Sym.of("·", SURF, OCEAN),
Sym.of("•", SURF, OCEAN),
Sym.of("o", SURF, OCEAN),
Sym.of("O", SURF, OCEAN),
Sym.of("©", SURF, OCEAN),
];
constructor() {
super("Fountain", 0, NONE, Fountain.SYMBOLS[4]);
}
randomSeed(): boolean {
return true;
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
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() {
return new Fountain();
}
tag() {
return "Room Features";
}
})();
}
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);
}