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