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);
}