import { Decorator } from "../pieces/terrain/decorators.ts";
import { Cell } from "./cell.ts";
import { Direction } from "./direction.ts";
import { GameEvent } from "./game-event.ts";
import { Registry } from "./registry.ts";
import { State } from "./state.ts";
import { Terrain } from "./terrain.ts";
import { COLUMNS, ROWS } from "./board.ts";
/**
* Utility helpers for terrain manipulation.
* Mirrors TerrainUtils.java.
*/
export const TerrainUtils = {
/**
* Remove a decorator from every cell on the board where it appears,
* replacing it with the decorator's proxied (wrapped) terrain.
*
* Used by TriggerOnce and similar one-shot decorators.
*/
removeDecorator(event: GameEvent, terrain: Terrain) {
event.board.visit((cell) => {
if (cell.terrain === terrain && terrain instanceof Decorator) {
// terrain is a Decorator — replace it with the proxied terrain
cell.setTerrain(terrain.getProxiedTerrain());
}
return true; // keep visiting
});
},
/**
* Toggle the state of the terrain in `cell` (ON→OFF or OFF→ON).
* Does nothing if an agent occupies the cell.
*/
toggleCellState(cell: Cell, terrain: Terrain, state: State): Terrain | null {
if (cell.agent == null) {
const other = this.getTerrainOtherState(terrain, state);
cell.setTerrain(other);
return other;
}
return null;
},
/**
* Return the same terrain type but with its state toggled (ON↔OFF).
* Works by manipulating the Registry serialization key: splits on "|" and
* replaces the segment that exactly equals "on"/"off" with the other,
* then looks up the new key. Splitting on the pipe delimiter (rather than
* regexing across the whole string) avoids misfiring on a free-text arg
* that happens to be exactly that word (a Pylon's board path, an NPC's
* greeting text).
*/
getTerrainOtherState(terrain: Terrain, state: State): Terrain {
const target = state.isOn() ? "on" : "off";
const replacement = state.isOn() ? "off" : "on";
const key = Registry.serialize(terrain)
.split("|")
.map((segment) => (segment === target ? replacement : segment))
.join("|");
return Registry.terrain(key);
},
/**
* Return the cell on the opposite side of the board from `cell`
* when traveling in `direction`. Used by rolling-boulder wrap-around.
*
* A diagonal direction (e.g. NORTHEAST) matches two of the four checks
* below, so tie-break order matters: at the top/bottom edge, vertical
* wrap wins; everywhere else, horizontal wrap wins. Existing tests pin
* this exact order.
*/
getCellOnOppositeSide(cell: Cell, direction: Direction): Cell | null {
const wrapNorth = () => cell.board.getCellAt(cell.x, ROWS - 1);
const wrapSouth = () => cell.board.getCellAt(cell.x, 0);
const wrapEast = () => cell.board.getCellAt(0, cell.y);
const wrapWest = () => cell.board.getCellAt(COLUMNS - 1, cell.y);
const checks: Array<[boolean, () => Cell | null]> =
cell.y === 0 || cell.y === ROWS - 1
? [
[direction.isNortherly(), wrapNorth],
[direction.isSoutherly(), wrapSouth],
[direction.isEasterly(), wrapEast],
[direction.isWesterly(), wrapWest],
]
: [
[direction.isEasterly(), wrapEast],
[direction.isWesterly(), wrapWest],
[direction.isNortherly(), wrapNorth],
[direction.isSoutherly(), wrapSouth],
];
for (const [matches, wrap] of checks) {
if (matches) return wrap();
}
return null;
},
};