import { Board } from "../core/board.ts";
import { Registry } from "../core/registry.ts";
import { Terrain } from "../core/terrain.ts";
import { Agent } from "../core/agent.ts";
import { Item } from "../core/item.ts";
import { ROWS, COLUMNS } from "../core/board.ts";
const isNode = (globalThis as any).process?.versions?.node;
const BASE_URL = isNode
? import.meta.url.split("/game/")[0]
: window.location.href.split("/game/")[0];
export type SaveFile = {
board: Board,
startX: number,
startY: number,
startInv: string[]
}
/**
* Load a board from a scenario JSON file.
*/
export async function loadBoard(boardPath: string, baseURL: string = BASE_URL): Promise<SaveFile> {
const url = `${baseURL}/public/scenarios/${boardPath}.json`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load board: ${url} (${response.status})`);
}
const contentType = response.headers.get("content-type") ?? "";
if (
!contentType.includes("application/json") &&
!contentType.includes("text/json")
) {
throw new Error(
`Failed to load board: ${url} (response was not JSON — board file may be missing)`,
);
}
const data = await response.json();
return buildBoard(data, boardPath);
}
/**
* Build a Board from a deserialized scenario data object.
* Also used to restore boards from player.unsavedMaps.
*/
export function buildBoard(data: any, boardPath: string): SaveFile {
const board = new Board();
board.outside = data.outside ?? false;
board.startX = data.startX ?? 0;
board.startY = data.startY ?? 0;
board.scenarioName = data.scenarioName ?? data.name ?? null;
board.creator = data.creator ?? null;
board.description = data.description ?? null;
board.startInv = data.startInv ?? null;
board.folder = data.folder ?? null;
board.boardID = boardPath;
// Parse diagram: diagram[y][x] gives the char for cell (x, y). Rows/columns
// with no data are left untouched (used by tests that don't care about
// terrain), but a character that IS present must resolve to real terrain —
// an unmapped or mistyped token means the scenario file is malformed.
const terrainMap = data.terrain ?? {};
for (let y = 0; y < ROWS; y++) {
const row = data.diagram?.[y];
if (row == null) continue;
for (let x = 0; x < COLUMNS; x++) {
const ch = row[x];
if (ch == null) continue;
const terrainKey = terrainMap[ch];
if (!terrainKey) {
throw new Error(`Token "${ch}" not mapped to a piece`);
}
const piece = Registry.get(terrainKey);
if (!(piece instanceof Terrain)) {
throw new Error(`The key ${terrainKey} is not a valid terrain.`);
}
board.cells[x][y].setTerrain(piece);
}
}
// Place pieces (agents/items) from the pieces array
for (const pd of data.pieces ?? []) {
if (pd.key == null) continue;
const piece = Registry.get(pd.key);
const cell = board.getCellAt(pd.x, pd.y);
if (!cell || !piece) continue;
if (piece instanceof Agent) {
if (!cell.agent) cell.setAgent(piece);
} else if (piece instanceof Item) {
cell.addItem(piece);
} else if (piece instanceof Terrain) {
cell.setTerrain(piece);
}
}
// Register adjacent board paths
const boardDir = boardPath.includes("/")
? boardPath.split("/").slice(0, -1).join("/")
: "";
for (const dirName of ["north", "south", "east", "west", "up", "down"]) {
const stem = data[dirName];
if (stem) {
const fullPath = boardDir ? `${boardDir}/${stem}` : stem;
board.setAdjacentBoard(dirName, fullPath);
}
}
const startInventory = data.startInv
? data.startInv.split(",").map((s: string) => s.trim())
: [];
return {
board,
startX: data.startX ?? 0,
startY: data.startY ?? 0,
startInv: startInventory,
};
}