persistence/loader.ts

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