import { Crowbar, Crystal, Grenade, Key } from "../items/items.js";
import { oscillate } from "../effects/effects.js";
import { AbstractBoulder } from "../../pieces/agents/creatures.js";
import { Terrain } from "../../core/terrain.js";
import { Registry } from "../../core/registry.js";
import { TerrainUtils } from "../../core/terrain-utils.js";
import { events } from "../../core/events.js";
import {
TRAVERSABLE,
PENETRABLE,
VERTICAL,
DETECT_HIDDEN,
AMMUNITION,
} from "../../core/flags.js";
import {
WHITE,
BLACK,
NEARBLACK,
BARELY_BUILDING_WALL,
BUILDING_FLOOR,
BUILDING_WALL,
BURLYWOOD,
BURNTWOOD,
DARKGOLDENROD,
DARKGRAY,
DARKSLATEGRAY,
LIGHTSLATEGRAY,
LIMEGREEN,
LOW_ROCKS,
SADDLEBROWN,
SANDYBROWN,
STEELBLUE,
colorByName,
NONE,
} from "../../core/color.js";
import { Symbol } from "../../core/symbol.js";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.js";
import { ON, OFF, stateFromString } from "../../core/state.js";
import {
directionByName,
NONE as DIR_NONE,
WEST,
EAST,
} from "../../core/direction.js";
// ── Door ─────────────────────────────────────────────────────────────────────
/**
* 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 {
constructor(color, state) {
const sym = state.isOn()
? Symbol.of("·", color, null, color, BUILDING_FLOOR)
: Symbol.of("·", WHITE, color, BUILDING_FLOOR, color);
super(color.name + " Door", 0, color, sym);
this.state = state;
}
onColorEvent(_event, color, cell) {
if (color !== this.color) return;
TerrainUtils.toggleCellState(cell, this, this.state);
}
canEnter(_agent, _cell, direction) {
return this.state.isOn() && !direction.isDiagonal();
}
canExit(_agent, _cell, direction) {
return !direction.isDiagonal();
}
onEnter(event, player, cell, dir) {
if (dir.isDiagonal() || dir.isVertical()) {
event.cancel();
return;
}
const item = player.bag.getSelected?.() ?? player.bag.selected;
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();
} else {
event.cancelWithMessage(cell, "The door is locked.");
}
} else {
if (
item instanceof Key &&
item.color === this.color &&
cell.agent == null
) {
TerrainUtils.toggleCellState(cell, this, this.state);
player.bag.remove(item);
event.cancelWithMessage(cell, "You lock the door.");
}
}
}
onExit(event, _player, _cell, dir) {
if (dir.isDiagonal() || dir.isVertical()) event.cancel();
}
onAgentEnter(event, agent, cell, dir) {
if (agent instanceof AbstractBoulder) {
event.cancelWithMessage(cell, "It's too big.");
} else if (dir.isDiagonal() || dir.isVertical() || this.state.isOff()) {
event.cancel();
}
}
onAgentExit(event, _agent, _cell, dir) {
if (dir.isDiagonal() || dir.isVertical()) event.cancel();
}
onFlyOver(event, _cell, flier) {
if (this.state.isOff() || flier.direction?.isDiagonal()) event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state]) {
return new Door(colorByName(color) ?? NONE, stateFromString(state));
}
store(d) {
return `Door|${d.color.name}|${d.state.name}`;
}
example() {
return new Door(NONE, OFF);
}
template(_id) {
return "Door|{color}|{state}";
}
tag() {
return "Room Features";
}
})();
}
// ── Gate ─────────────────────────────────────────────────────────────────────
/**
* A gate that agents can open by walking through it straight-on.
* It auto-closes after the agent passes.
*/
export class Gate extends Terrain {
constructor(state) {
const sym = state.isOff()
? Symbol.of("#", WHITE, null, BLACK, BUILDING_FLOOR)
: state.isOn()
? Symbol.of("#", NEARBLACK, null, BUILDING_WALL, BUILDING_FLOOR)
: null;
super("Gate", TRAVERSABLE | PENETRABLE, sym);
this.state = state;
}
canEnter(_agent, _cell, direction) {
return !direction.isDiagonal();
}
canExit(_agent, _cell, direction) {
return !direction.isDiagonal();
}
onEnter(event, _player, cell, dir) {
if (dir.isDiagonal()) {
event.cancel();
return;
}
if (this.state.isOff()) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.cancel();
events.fireMessage(cell, "You open the gate");
}
}
onExit(event, _player, cell, dir) {
if (dir.isDiagonal()) {
event.cancel();
return;
}
TerrainUtils.toggleCellState(cell, this, this.state);
}
onAgentEnter(event, agent, cell, dir) {
if (agent instanceof AbstractBoulder) {
event.cancelWithMessage(cell, "It's too big.");
} else if (this.state.isOff()) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.cancel();
}
}
onAgentExit(_event, _agent, cell, _dir) {
TerrainUtils.toggleCellState(cell, this, this.state);
}
onFlyOver(event, _cell, flier) {
if (flier.direction?.isDiagonal()) event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create([state]) {
return new Gate(stateFromString(state));
}
store(g) {
return `Gate|${g.state.name}`;
}
example() {
return new Gate(OFF);
}
template(_id) {
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,
null,
Symbol.of("#", SANDYBROWN, null, SADDLEBROWN, BUILDING_FLOOR),
);
}
onEnter(event, _player, cell, _dir) {
event.cancelWithMessage(cell, "The gate is too rusty to open");
}
onFlyOver(event, _cell, flier) {
if (flier.direction.isDiagonal()) {
event.cancel();
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("RustyGate");
}
create(_args) {
return new RustyGate();
}
tag() {
return "Room Features";
}
})();
}
// ── Stairs / Vertical exits ───────────────────────────────────────────────────
class StairsUp extends Terrain {
constructor() {
super(
"Stairs Up",
TRAVERSABLE | PENETRABLE | VERTICAL,
Symbol.of("<", WHITE, null, BLACK, BUILDING_FLOOR),
);
}
onEnter(event, _player, cell, _dir) {
events.fireMessage(cell, "Use 'z' to go up");
}
onExit(event, _player, _cell, dir) {
if (dir.isVertical() && dir.name === "down") event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("StairsUp");
}
create(_args) {
return new StairsUp();
}
tag() {
return "Room Features";
}
})();
}
class StairsDown extends Terrain {
constructor() {
super(
"Stairs Down",
TRAVERSABLE | PENETRABLE | VERTICAL,
Symbol.of(">", WHITE, null, BLACK, BUILDING_FLOOR),
);
}
onEnter(event, _player, cell, _dir) {
events.fireMessage(cell, "Use 'z' to go down");
}
onExit(event, _player, _cell, dir) {
if (dir.isVertical() && dir.name === "up") event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("StairsDown");
}
create(_args) {
return new StairsDown();
}
tag() {
return "Room Features";
}
})();
}
class CaveEntrance extends Terrain {
constructor() {
super(
"Cave Entrance",
TRAVERSABLE | PENETRABLE | VERTICAL,
Symbol.of("∩", WHITE, NONE, NEARBLACK, LOW_ROCKS),
);
}
onEnter(event, _player, cell, _dir) {
const msg = event.board?.outside
? "Use 'z' to enter the cave"
: "Use 'z' to exit the cave";
events.fireMessage(cell, msg);
}
onExit(event, _player, _cell, dir) {
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(_args) {
return new CaveEntrance();
}
tag() {
return "Outside Terrain";
}
})();
}
// ── Containers ────────────────────────────────────────────────────────────────
/**
* Locked chest. Requires a matching color key.
* item and count may be null (empty chest).
*/
export class Chest extends Terrain {
constructor(item, count, color) {
super(
`${color.name} Chest`,
PENETRABLE,
color,
Symbol.of("⊠", color, null, color, BUILDING_FLOOR),
);
this.item = item;
this.count = count ?? 1;
}
onEnter(event, player, cell, _dir) {
if (!cell.isBagEmpty) return;
const sel = player.bag.getSelected?.() ?? player.bag.selected;
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(cell, "The key is not the right color");
}
} else {
events.fireMessage(cell, "A large locked chest blocks your way");
}
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args) {
if (args.length === 1)
return new Chest(null, 1, colorByName(args[0]) ?? NONE);
if (args.length === 2)
return new Chest(_ri(args[0]), 1, colorByName(args[1]) ?? NONE);
return new Chest(
_ri(args[0]),
parseInt(args[1]) || 1,
colorByName(args[2]) ?? NONE,
);
}
store(c) {
return c.item
? `Chest|${this.esc(c.item)}|${c.count}|${c.color.name}`
: `Chest|${c.color.name}`;
}
example() {
return new Chest(null, 1, NONE);
}
template(_id) {
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,
Symbol.of("≠", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Boards");
}
create(_args) {
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,
Symbol.of("⊔", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("EmptyChest");
}
create(_args) {
return new EmptyChest();
}
tag() {
return "Room Features";
}
})();
}
/**
* Crate — requires a crowbar to open.
* item and count may be null.
*/
export class Crate extends Terrain {
constructor(item, count) {
super(
"Crate",
PENETRABLE,
Symbol.of("⊠", WHITE, null, NEARBLACK, BUILDING_FLOOR),
);
this.item = item;
this.count = count ?? 1;
}
onEnter(event, player, cell, _dir) {
if (!cell.isBagEmpty) return;
const sel = player.bag.getSelected?.() ?? player.bag.selected;
if (sel instanceof Crowbar) {
cell.openContainer("crate", this.item, this.count, "Boards", player);
} else {
events.fireMessage(
cell,
"A large crate. It can't be moved, but try prying it open",
);
}
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args) {
if (args.length === 0) return new Crate(null, 1);
if (args.length === 1) return new Crate(_ri(args[0]), 1);
return new Crate(_ri(args[0]), parseInt(args[1]) || 1);
}
store(c) {
return c.item ? `Crate|${this.esc(c.item)}|${c.count}` : "Crate";
}
example() {
return new Crate(null, 1);
}
template(_id) {
return "Crate|{item}|{count?}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Urn — smashable container. Item inside; breakable by any attack.
* item may be null.
*/
export class Urn extends Terrain {
constructor(item) {
super(
"Urn",
Symbol.of("u", DARKGOLDENROD, BLACK, DARKGOLDENROD, BUILDING_FLOOR),
);
this.item = item;
}
onEnter(event, player, cell, _dir) {
cell.openContainer("urn", this.item, 1, "EmptyChest", player);
event.cancel();
}
onFlyOver(event, cell, _flier) {
cell.openContainer("urn", this.item, 1, "EmptyChest", null);
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args) {
return new Urn(args.length > 0 ? _ri(args[0]) : null);
}
store(u) {
return u.item ? `Urn|${this.esc(u.item)}` : "Urn";
}
example() {
return new Urn(null);
}
template(_id) {
return "Urn|{item?}";
}
tag() {
return "Room Features";
}
})();
}
// ── Switches / Triggers ───────────────────────────────────────────────────────
/**
* Switch — fires a color event; toggles appearance.
* Can be triggered by throwing a non-ammunition item at it.
*/
export class Switch extends Terrain {
constructor(color, state) {
const entity = state.isOn() ? "!" : "\u00A1"; // ¡
super(color.name + " Switch", 0, color, Symbol.of(entity, BLACK, color));
this.state = state;
}
onEnter(event, _player, cell, dir) {
event.cancel();
if (dir.isDiagonal()) {
events.fireMessage(cell, "You must use it square on");
return;
}
TerrainUtils.toggleCellState(cell, this, this.state);
event.board.fireColorEvent(event, this.color, cell);
}
onFlyOver(event, cell, flier) {
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]) {
return new Switch(colorByName(color) ?? NONE, stateFromString(state));
}
store(s) {
return `Switch|${s.color.name}|${s.state.name}`;
}
example() {
return new Switch(NONE, OFF);
}
template(_id) {
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 {
constructor(color, state) {
const entity = state.isOn() ? "?" : "\u00BF"; // ¿
super(
color.name + " Key Switch",
0,
color,
Symbol.of(entity, BLACK, color),
);
this.state = state;
}
onEnter(event, player, cell, dir) {
event.cancel();
if (dir.isDiagonal()) {
events.fireMessage(cell, "You must use it square on");
return;
}
const sel = player.bag.getSelected?.() ?? player.bag.selected;
if (sel instanceof Key && sel.color === this.color) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.board.fireColorEvent(event, this.color, cell);
player.bag.remove(sel);
} else {
events.fireMessage(cell, "You need the right key");
}
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state]) {
return new KeySwitch(colorByName(color) ?? NONE, stateFromString(state));
}
store(ks) {
return `KeySwitch|${ks.color.name}|${ks.state.name}`;
}
example() {
return new KeySwitch(NONE, OFF);
}
template(_id) {
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) {
super(
"Pressure Plate",
TRAVERSABLE | PENETRABLE,
color,
Symbol.of("Ξ", NEARBLACK, BLACK, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
this._pressColor = color;
}
onEnter(event, _player, cell, _dir) {
event.board.fireColorEvent(event, this._pressColor, cell);
}
onExit(event, _player, cell, _dir) {
event.board.fireColorEvent(event, this._pressColor, cell);
}
onAgentEnter(event, _agent, cell, _dir) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this._pressColor, cell);
}
onAgentExit(event, _agent, cell, _dir) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this._pressColor, cell);
}
static SERIALIZER = new (class extends BaseSerializer {
create([color]) {
return new PressurePlate(colorByName(color) ?? NONE);
}
store(pp) {
return `PressurePlate|${pp._pressColor.name}`;
}
example() {
return new PressurePlate(NONE);
}
template(_id) {
return "PressurePlate|{color}";
}
tag() {
return "Room Features";
}
})();
}
// ── Decorative / Interactive ──────────────────────────────────────────────────
/**
* Altar — cosmetic 3-part piece (WEST bracket, NONE center π, EAST bracket).
* Pass direction string "west", "none", or "east".
*/
export class Altar extends Terrain {
constructor(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,
Symbol.of(entity, fg, DARKSLATEGRAY),
);
this._direction = direction;
}
static SERIALIZER = new (class extends BaseSerializer {
create([direction]) {
return new Altar(directionByName(direction) ?? DIR_NONE);
}
store(a) {
return `Altar|${a._direction?.name ?? String(a._direction)}`;
}
example() {
return new Altar(DIR_NONE);
}
template(_id) {
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 = Symbol.of(
"≣",
BURLYWOOD,
BLACK,
BURNTWOOD,
BUILDING_FLOOR,
);
static MARKED_SYM = Symbol.of(
"≡",
LIMEGREEN,
BLACK,
LIMEGREEN,
BUILDING_FLOOR,
);
constructor(item) {
super("Bookshelf", Bookshelf.UNMARKED_SYM);
this.item = item;
}
onEnter(event, player, cell, dir) {
if (dir.isDiagonal()) {
event.cancelWithMessage("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.get("Bookshelf"));
} else {
events.fireMessage(cell, "You find nothing on the bookshelf.");
}
}
onAdjacentTo(event, cell) {
if (this.item != null && event.player.is(DETECT_HIDDEN)) {
// Re-render with marked symbol via cell change notification
this.symbol = Bookshelf.MARKED_SYM;
cell.board._notifyCellChange(cell);
}
}
onNotAdjacentTo(_event, cell) {
this.symbol = Bookshelf.UNMARKED_SYM;
cell.board._notifyCellChange(cell);
}
static SERIALIZER = new (class extends BaseSerializer {
create(args) {
return new Bookshelf(args.length > 0 ? _ri(args[0]) : null);
}
store(b) {
return b.item ? `Bookshelf|${this.esc(b.item)}` : "Bookshelf";
}
example() {
return new Bookshelf(null);
}
template(_id) {
return "Bookshelf|{item?}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Pit — impassable. Boulders fill it; other items fall in (disappear).
*/
class Pit extends Terrain {
constructor() {
super(
"Pit",
PENETRABLE,
Symbol.of("U", NEARBLACK, BLACK, BLACK, BUILDING_FLOOR),
);
}
canEnter(agent, _cell, _direction) {
return agent instanceof AbstractBoulder;
}
onEnter(event, _player, cell, _dir) {
console.log(_player);
event.cancelWithMessage(cell, "You'd fall into the pit");
}
onAgentEnter(event, agent, cell, dir) {
if (agent instanceof AbstractBoulder) {
const prevCell = cell.getAdjacentCell(dir.reverse);
prevCell?.removeAgent(agent);
cell.setTerrain(Registry.get("Floor"));
events.fireMessage(cell, "The pit is filled by the boulder");
} else if (agent.isSlider || agent.isPusher) {
const prevCell = cell.getAdjacentCell(dir.reverse);
prevCell?.removeAgent(agent);
events.fireMessage(
cell,
`The ${agent.name.toLowerCase()} falls through the pit`,
);
} else {
event.cancel();
}
}
onDrop(event, cell, item) {
if (!(item instanceof Grenade)) {
event.cancelWithMessage(
cell,
item.getDefiniteNoun("{0} falls into the pit"),
);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Pit");
}
create(_args) {
return new Pit();
}
tag() {
return "Room Features";
}
})();
}
// ── Pylon ────────────────────────────────────────────────────────────────────
/**
* 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 {
constructor(color, state, boardID, x, y) {
super(
color.name + " Pylon",
0,
color,
Symbol.of("Δ", color, null, color, BUILDING_FLOOR),
);
this.state = state;
this._boardID = boardID;
this._x = x;
this._y = y;
}
randomSeed() {
return true;
}
onEnter(event, player, cell, dir) {
if (this.state.isOff()) {
event.cancel();
const item = player.bag.getSelected?.();
if (item instanceof Key && item.color === this.color) {
event.cancelWithMessage(cell, "Pylons cannot be activated with keys.");
} else if (item instanceof Crystal && item.color === this.color) {
player.bag.remove(item);
TerrainUtils.toggleCellState(cell, this, this.state);
} else {
event.cancelWithMessage(cell, "The pylon must be activated.");
}
} else if (dir != null) {
player.teleport(event, dir, this._boardID, this._x, this._y);
}
}
onExit(_event, _player, cell, _dir) {
if (this.state.isOff()) {
const other = TerrainUtils.getTerrainOtherState(cell.terrain, this.state);
cell.setTerrain(other);
}
}
onFrame(_event, cell, frame) {
if (this.state.isOn()) {
const outside = cell.board.outside ?? false;
const bg = cell.terrain.symbol.getBackground(outside);
const nfg = oscillate(this.color, null, 15, frame);
const nbg = oscillate(bg, this.color, 15, frame);
cell._animTerrainSymbol = Symbol.of("Δ", nfg, nbg, nfg, nbg);
cell.board._notifyCellChange(cell);
}
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state, boardID, x, y]) {
return new Pylon(
colorByName(color) ?? NONE,
stateFromString(state),
boardID,
parseInt(x) || 0,
parseInt(y) || 0,
);
}
store(p) {
return `Pylon|${p.color.name}|${p.state.name}|${p._boardID}|${p._x}|${p._y}`;
}
example() {
return new Pylon(STEELBLUE, OFF, "", 0, 0);
}
template(_id) {
return "Pylon|{color}|{state}|{boardID}|{x}|{y}";
}
tag() {
return "Room Features";
}
})();
}
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Resolve an item arg: if it's a plain string key, look it up in the Registry. */
function _ri(arg) {
return typeof arg === "string" ? Registry.get(arg) : arg;
}
// ── Registry ──────────────────────────────────────────────────────────────────
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);
}