import {
Arrow,
Bullet,
Crowbar,
Crystal,
Grenade,
Key,
} from "../items/items.ts";
import { InFlightItem, oscillate, Smash } from "../effects/effects.ts";
import {
AbstractBoulder,
Slider,
Pusher,
} from "../agents/creatures.ts";
import { Terrain } from "../../core/terrain.ts";
import { Registry } from "../../core/registry.ts";
import { TerrainUtils } from "../../core/terrain-utils.ts";
import { events } from "../../core/event-bus.ts";
import {
TRAVERSABLE,
PENETRABLE,
VERTICAL,
DETECT_HIDDEN,
AMMUNITION,
MELEE_WEAPON,
} from "../../core/flags.ts";
import {
WHITE,
BLACK,
NEARBLACK,
BARELY_BUILDING_WALL,
BUILDING_FLOOR,
BUILDING_WALL,
BURLYWOOD,
BURNTWOOD,
DARKGOLDENROD,
DARKGRAY,
DARKSLATEGRAY,
LIGHTSLATEGRAY,
LIMEGREEN,
LOW_ROCKS,
RED,
SADDLEBROWN,
SANDYBROWN,
SILVER,
STEELBLUE,
colorByName,
NONE,
Color,
} from "../../core/color.ts";
import { Sym } from "../../core/sym.ts";
import { TypeOnlySerializer, BaseSerializer } from "../../core/serializer.ts";
import { ON, OFF, stateFromString, State } from "../../core/state.ts";
import {
directionByName,
NONE as DIR_NONE,
WEST,
EAST,
Direction,
} from "../../core/direction.ts";
import { GameEvent } from "../../core/game-event.ts";
import { Cell } from "../../core/cell.ts";
import { Agent } from "../../core/agent.ts";
import { Player } from "../../core/player.ts";
import { Item } from "../../core/item.ts";
import { Rubble } from "./decorators.ts";
import { Floor } from "./basic.ts";
import { Piece } from "../../core/piece.ts";
import { Animated } from "../../core/animated.ts";
/**
* 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 {
state: State;
constructor(color: Color, state: State) {
const sym = state.isOn()
? Sym.of("⸱", color, null, color, BUILDING_FLOOR)
: Sym.of("⸱", WHITE, color, BUILDING_FLOOR, color);
super(color.name + " Door", 0, color, sym);
this.state = state;
}
onColorEvent(event: GameEvent, color: Color, cell: Cell) {
if (color !== this.color) return;
TerrainUtils.toggleCellState(cell, this, this.state);
}
canEnter(agent: Agent, cell: Cell, direction: Direction) {
return this.state.isOn() && !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();
return;
}
const item = player.bag.getSelected();
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("Door unlocks.", cell);
} else {
event.cancel("The door is locked.", cell);
}
} else {
if (
item instanceof Key &&
item.color === this.color &&
cell.agent == null
) {
TerrainUtils.toggleCellState(cell, this, this.state);
player.bag.remove(item);
event.cancel("You lock the door.", cell);
}
}
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (dir.isDiagonal() || dir.isVertical()) event.cancel();
}
onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (agent instanceof AbstractBoulder) {
event.cancel("It's too big.", cell);
} else if (dir.isDiagonal() || dir.isVertical() || this.state.isOff()) {
event.cancel();
}
}
onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (dir.isDiagonal() || dir.isVertical()) event.cancel();
}
onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
if (this.state.isOff() || flier.direction.isDiagonal()) event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state]: string[]) {
return new Door(colorByName(color) ?? NONE, stateFromString(state));
}
store(d: Door) {
return `Door|${d.color.name}|${d.state.name}`;
}
example() {
return new Door(STEELBLUE, OFF);
}
template() {
return "Door|{color}|{state}";
}
tag() {
return "Room Features";
}
})();
}
/**
* A gate that agents can open by walking through it straight-on.
* It auto-closes after the agent passes.
*/
export class Gate extends Terrain {
state: State;
constructor(state: State) {
const sym = state.isOff()
? Sym.of("#", WHITE, null, BLACK, BUILDING_FLOOR)
: Sym.of("#", NEARBLACK, null, BUILDING_WALL, BUILDING_FLOOR);
super("Gate", TRAVERSABLE | PENETRABLE, NONE, sym);
this.state = state;
}
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()) {
event.cancel();
return;
}
if (this.state.isOff()) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.cancel();
events.fireMessage("You open the gate", cell);
}
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (dir.isDiagonal()) {
event.cancel();
return;
}
TerrainUtils.toggleCellState(cell, this, this.state);
}
onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (agent instanceof AbstractBoulder) {
event.cancel("It's too big.", cell);
} else if (this.state.isOff()) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.cancel();
}
}
onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
TerrainUtils.toggleCellState(cell, this, this.state);
}
onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
if (flier.direction.isDiagonal()) event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create([state]: string[]) {
return new Gate(stateFromString(state));
}
store(g: Gate) {
return `Gate|${g.state.name}`;
}
example() {
return new Gate(ON);
}
template() {
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,
NONE,
Sym.of("#", SANDYBROWN, null, SADDLEBROWN, BUILDING_FLOOR),
);
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
event.cancel("The gate is too rusty to open", cell);
}
onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
if (flier.direction.isDiagonal()) {
event.cancel();
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("RustyGate");
}
create() {
return new RustyGate();
}
tag() {
return "Room Features";
}
})();
}
class StairsUp extends Terrain {
constructor() {
super(
"Stairs Up",
TRAVERSABLE | PENETRABLE | VERTICAL,
NONE,
Sym.of("<", WHITE, null, BLACK, BUILDING_FLOOR),
);
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
events.fireMessage("Use 'z' to go up", cell);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (dir.isVertical() && dir.name === "down") event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("StairsUp");
}
create() {
return new StairsUp();
}
tag() {
return "Room Features";
}
})();
}
class StairsDown extends Terrain {
constructor() {
super(
"Stairs Down",
TRAVERSABLE | PENETRABLE | VERTICAL,
NONE,
Sym.of(">", WHITE, null, BLACK, BUILDING_FLOOR),
);
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
events.fireMessage("Use 'z' to go down", cell);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (dir.isVertical() && dir.name === "up") event.cancel();
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("StairsDown");
}
create() {
return new StairsDown();
}
tag() {
return "Room Features";
}
})();
}
class CaveEntrance extends Terrain {
constructor() {
super(
"Cave Entrance",
TRAVERSABLE | PENETRABLE | VERTICAL,
NONE,
Sym.of("∩", SILVER, NONE, NEARBLACK, LOW_ROCKS),
);
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
const msg = event.board.outside
? "Use 'z' to enter the cave"
: "Use 'z' to exit the cave";
events.fireMessage(msg, cell);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
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() {
return new CaveEntrance();
}
tag() {
return "Outside Terrain";
}
})();
}
/**
* Locked chest. Requires a matching color key.
* item and count may be null (empty chest).
*/
export class Chest extends Terrain {
item: Item | null;
count: number;
constructor(item: Item | null, count: number, color: Color) {
if (color === NONE) {
throw new Error("Chest must have a color");
}
super(
`${color.name} Chest`,
PENETRABLE,
color,
Sym.of("⊠", color, null, color, BUILDING_FLOOR),
);
this.item = item;
this.count = count;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (!cell.isBagEmpty) return;
const sel = player.bag.getSelected();
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("The key is not the right color", cell);
}
} else {
events.fireMessage("A large locked chest blocks your way", cell);
}
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args: string[]) {
if (args.length === 1)
return new Chest(null, 1, colorByName(args[0]) ?? NONE);
if (args.length === 2)
return new Chest(Registry.item(args[0]), 1, colorByName(args[1]) ?? NONE);
return new Chest(
Registry.item(args[0]),
parseInt(args[1]) || 1,
colorByName(args[2]) ?? NONE,
);
}
store(c: Chest) {
return c.item
? `Chest|${this.esc(c.item)}|${c.count}|${c.color.name}`
: `Chest|${c.color.name}`;
}
example() {
return new Chest(null, 1, STEELBLUE);
}
template() {
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,
NONE,
Sym.of("≠", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Boards");
}
create() {
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,
NONE,
Sym.of("⊔", NEARBLACK, null, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("EmptyChest");
}
create() {
return new EmptyChest();
}
tag() {
return "Room Features";
}
})();
}
/**
* Crate — requires a crowbar to open.
* item and count may be null.
*/
export class Crate extends Terrain {
item: Item | null;
count: number;
constructor(item: Item | null, count: number | null) {
super(
"Crate",
PENETRABLE,
NONE,
Sym.of("⊠", WHITE, null, NEARBLACK, BUILDING_FLOOR),
);
this.item = item;
this.count = count ?? 1
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (!cell.isBagEmpty) return;
const sel = player.bag.getSelected();
if (sel instanceof Crowbar) {
cell.openContainer("crate", this.item, this.count, "Boards", player);
} else {
events.fireMessage("A large crate. It can't be moved, but try prying it open", cell);
}
event.cancel();
}
static SERIALIZER = new (class extends BaseSerializer {
create(args: string[]) {
if (args.length === 0) return new Crate(null, 1);
if (args.length === 1) return new Crate(Registry.item(args[0]), 1);
return new Crate(Registry.item(args[0]), parseInt(args[1]) || 1);
}
store(c: Crate) {
return c.item ? `Crate|${this.esc(c.item)}|${c.count}` : "Crate";
}
example() {
return new Crate(null, 1);
}
template() {
return "Crate|{item}|{count?}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Urn — smashable container. Requires a melee weapon to smash; projectiles
* (arrows, bullets) also smash it. Matches Java Urn behavior.
*/
export class Urn extends Terrain {
#item: Item | null;
constructor(item: Item | null) {
super(
"Urn",
0,
NONE,
Sym.of("𐃡", DARKGOLDENROD, BLACK, DARKGOLDENROD, BUILDING_FLOOR),
);
this.#item = item;
}
get item() {
return this.#item;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
const sel = player.bag.getSelected();
if (sel.is(MELEE_WEAPON)) {
events.fireMessage("You smash open the urn", cell);
this.#smashUrn(cell);
} else {
events.fireMessage("The urn is too large", cell);
}
event.cancel();
}
onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
if (flier.item instanceof Arrow || flier.item instanceof Bullet) {
event.cancel();
this.#smashUrn(cell);
}
}
#smashUrn(cell: Cell) {
if (this.#item !== null) {
cell.addItem(this.#item);
}
cell.addEffect(new Smash());
cell.setTerrain(Registry.terrain("Rubble|Floor"));
}
static SERIALIZER = new (class extends BaseSerializer {
create(args: string[]) {
return new Urn(args.length > 0 ? Registry.item(args[0]) : null);
}
store(u: Urn) {
return u.#item ? `Urn|${this.esc(u.#item)}` : "Urn";
}
example() {
return new Urn(null);
}
template() {
return "Urn|{item?}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Switch — fires a color event; toggles appearance.
* Can be triggered by throwing a non-ammunition item at it.
*/
export class Switch extends Terrain {
state: State;
constructor(color: Color, state: State) {
const entity = state.isOn() ? "!" : "\u00A1"; // ¡
super(color.name + " Switch", 0, color, Sym.of(entity, BLACK, color));
this.state = state;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
event.cancel();
if (dir.isDiagonal()) {
events.fireMessage("You must use it square on", cell);
return;
}
TerrainUtils.toggleCellState(cell, this, this.state);
event.board.fireColorEvent(event, this.color, cell);
}
onFlyOver(event: GameEvent, cell: Cell, flier: InFlightItem) {
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]: string[]) {
return new Switch(colorByName(color) ?? NONE, stateFromString(state));
}
store(s: Switch) {
return `Switch|${s.color.name}|${s.state.name}`;
}
example() {
return new Switch(STEELBLUE, ON);
}
template() {
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 {
state: State;
constructor(color: Color, state: State) {
const entity = state.isOn() ? "?" : "\u00BF"; // ¿
super(color.name + " Key Switch", 0, color, Sym.of(entity, BLACK, color));
this.state = state;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
event.cancel();
if (dir.isDiagonal()) {
events.fireMessage("You must use it square on", cell);
return;
}
const sel = player.bag.getSelected();
if (sel instanceof Key) {
if (sel.color === this.color) {
TerrainUtils.toggleCellState(cell, this, this.state);
event.board.fireColorEvent(event, this.color, cell);
player.bag.remove(sel);
} else {
events.fireMessage("That key is the wrong color", cell);
}
} else {
events.fireMessage("This switch requires a key to operate", cell);
}
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state]: string[]) {
return new KeySwitch(colorByName(color) ?? NONE, stateFromString(state));
}
store(ks: KeySwitch) {
return `KeySwitch|${ks.color.name}|${ks.state.name}`;
}
example() {
return new KeySwitch(STEELBLUE, ON);
}
template() {
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: Color) {
super(
"Pressure Plate",
TRAVERSABLE | PENETRABLE,
color,
Sym.of("Ξ", NEARBLACK, BLACK, BARELY_BUILDING_WALL, BUILDING_FLOOR),
);
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this.color, cell);
}
onExit(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this.color, cell);
}
onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this.color, cell);
}
onAgentExit(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (!event.isCancelled)
event.board.fireColorEvent(event, this.color, cell);
}
static SERIALIZER = new (class extends BaseSerializer {
create([color]: string[]) {
return new PressurePlate(colorByName(color) ?? NONE);
}
store(pp: PressurePlate) {
return `PressurePlate|${pp.color.name}`;
}
example() {
return new PressurePlate(RED);
}
template() {
return "PressurePlate|{color}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Altar — cosmetic 3-part piece (WEST bracket, NONE center π, EAST bracket).
* Pass direction string "west", "none", or "east".
*/
export class Altar extends Terrain {
#direction: Direction;
constructor(direction: 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, NONE, Sym.of(entity, fg, DARKSLATEGRAY));
this.#direction = direction;
}
static SERIALIZER = new (class extends BaseSerializer {
create([direction]: string[]) {
return new Altar(directionByName(direction) ?? DIR_NONE);
}
store(a: Altar) {
return `Altar|${a.#direction.name}`;
}
example() {
return new Altar(WEST);
}
template() {
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 = Sym.of(
"≣",
BURLYWOOD,
BLACK,
BURNTWOOD,
BUILDING_FLOOR,
);
static MARKED_SYM = Sym.of("≡", LIMEGREEN, BLACK, LIMEGREEN, BUILDING_FLOOR);
item: Item | null;
constructor(item: Item | null) {
super("Bookshelf", 0, NONE, Bookshelf.UNMARKED_SYM);
this.item = item;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (dir.isDiagonal()) {
event.cancel("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.terrain("Bookshelf"));
} else {
events.fireMessage("You find nothing on the bookshelf.", cell);
}
}
onAdjacentTo(event: GameEvent, cell: Cell) {
if (this.item != null && event.player.is(DETECT_HIDDEN)) {
cell.animTerrainSymbol = Bookshelf.MARKED_SYM;
cell.board.notifyCellChange(cell);
}
}
onNotAdjacentTo(event: GameEvent, cell: Cell) {
cell.animTerrainSymbol = null;
cell.board.notifyCellChange(cell);
}
static SERIALIZER = new (class extends BaseSerializer {
create(args: string[]) {
return new Bookshelf(args.length > 0 ? Registry.item(args[0]) as Item : null);
}
store(b: Bookshelf) {
return b.item ? `Bookshelf|${this.esc(b.item)}` : "Bookshelf";
}
example() {
return new Bookshelf(null);
}
template() {
return "Bookshelf|{item?}";
}
tag() {
return "Room Features";
}
})();
}
/**
* Pit — impassable. Boulders fill it; other items fall in (disappear).
*/
export class Pit extends Terrain {
constructor() {
super(
"Pit",
PENETRABLE,
NONE,
Sym.of("O", NEARBLACK, BLACK, BLACK, BUILDING_FLOOR),
);
}
canEnter(agent: Agent, cell: Cell, direction: Direction) {
return agent instanceof AbstractBoulder;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
event.cancel("You'd fall into the pit", cell);
}
onAgentEnter(event: GameEvent, agent: Agent, cell: Cell, dir: Direction) {
if (agent instanceof AbstractBoulder) {
const prevCell = cell.getAdjacentCell(dir.reverse);
prevCell?.removeAgent(agent);
cell.setTerrain(Registry.terrain("Floor"));
events.fireMessage("The pit is filled by the boulder", cell);
} else if (agent instanceof Slider || agent instanceof Pusher) {
const prevCell = cell.getAdjacentCell(dir.reverse);
prevCell?.removeAgent(agent);
events.fireMessage(`The ${agent.name.toLowerCase()} falls through the pit`, cell);
} else {
event.cancel();
}
}
onDrop(event: GameEvent, cell: Cell, item: Item) {
if (!(item instanceof Grenade)) {
event.cancel(
item.getDefiniteNoun("{0} falls into the pit"),
cell
);
}
}
static SERIALIZER = new (class extends TypeOnlySerializer {
constructor() {
super("Pit");
}
create() {
return new Pit();
}
tag() {
return "Room Features";
}
})();
}
/**
* 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 implements Animated {
state: State;
#boardID: string;
#x: number;
#y: number;
constructor(color: Color, state: State, boardID: string, x: number, y: number) {
super(
color.name + " Pylon",
0,
color,
Sym.of("Δ", color, null, color, BUILDING_FLOOR),
);
this.state = state;
this.#boardID = boardID;
this.#x = x;
this.#y = y;
}
animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation {
throw new Error("Method not implemented.");
}
getAnimations(options?: GetAnimationsOptions): Animation[] {
throw new Error("Method not implemented.");
}
randomSeed() {
return true;
}
onEnter(event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (this.state.isOff()) {
event.cancel();
const item = player.bag.getSelected();
if (item instanceof Key && item.color === this.color) {
event.cancel("Pylons cannot be activated with keys.", cell);
} else if (item instanceof Crystal && item.color === this.color) {
player.bag.remove(item);
TerrainUtils.toggleCellState(cell, this, this.state);
} else {
event.cancel("The pylon must be activated.", cell);
}
} else if (dir !== DIR_NONE) {
// dir is Direction.NONE when landing on the pylon (e.g. arriving via
// teleport), which is when we don't want to teleport again.
event.game.teleport(event, dir, this.#boardID, this.#x, this.#y);
}
}
onExit(_event: GameEvent, player: Player, cell: Cell, dir: Direction) {
if (this.state.isOff()) {
const other = TerrainUtils.getTerrainOtherState(cell.terrain!, this.state);
cell.setTerrain(other);
}
}
onFrame(event: GameEvent, cell: Cell, frame: number) {
if (this.state.isOn()) {
const outside = cell.board.outside;
const bg = cell.terrain.symbol.getBackground(outside);
if (bg) {
const nfg = oscillate(this.color, NONE, 15, frame);
const nbg = oscillate(bg, this.color, 15, frame);
cell.animTerrainSymbol = Sym.of("Δ", nfg, nbg, nfg, nbg);
cell.board.notifyCellChange(cell);
}
}
}
static SERIALIZER = new (class extends BaseSerializer {
create([color, state, boardID, x, y]: string[]) {
return new Pylon(
colorByName(color) ?? NONE,
stateFromString(state),
boardID,
parseInt(x) || 0,
parseInt(y) || 0,
);
}
store(p: Pylon) {
return `Pylon|${p.color.name}|${p.state.name}|${p.#boardID}|${p.#x}|${p.#y}`;
}
example() {
return new Pylon(STEELBLUE, OFF, "", 0, 0);
}
template() {
return "Pylon|{color}|{state}|{boardID}|{x}|{y}";
}
tag() {
return "Room Features";
}
})();
}
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);
}