// explore-viewer.js — walk a pre-baked first-person dungeon.
//
// Loads an atlas manifest (room graph + tiles) and the baked frames from
// /out/atlas/<seed>/, and lets you move through it Eye-of-the-Beholder style:
// you occupy a room and a cardinal facing; W/S step forward/back to the room in
// that direction, A/D turn. Each frame is the pre-rendered pixel-art view for the
// current (room, facing). Movement is just swapping the image — the dungeon was
// "burned off" ahead of time by tools/bake_atlas.py.

const params = new URLSearchParams(location.search);
const SEED = params.get('seed') || '7';
const BASE = `/out/atlas/${SEED}`;
const DIRS = ['N', 'E', 'S', 'W'];
const VEC = { N: [0, -1], E: [1, 0], S: [0, 1], W: [-1, 0] };

const img = document.getElementById('frame');
const hud = document.getElementById('hud');
const miss = document.getElementById('miss');
const missDetail = document.getElementById('miss-detail');
const mini = document.getElementById('minimap');
const mctx = mini.getContext('2d');

let manifest, byId;
const state = { room: null, facing: 'N' };

// One cache-bust token per page load: re-bakes show fresh art on reload, but
// turning/stepping within a session still hits the browser cache (snappy).
const BUST = `?t=${Date.now()}`;
const frameURL = (id, dir) => `${BASE}/r${id}_${dir}.png${BUST}`;
const room = () => byId.get(state.room);
const opposite = (d) => DIRS[(DIRS.indexOf(d) + 2) % 4];

async function boot() {
  try {
    manifest = await (await fetch(`${BASE}/manifest.json`)).json();
  } catch {
    hud.innerHTML = `no atlas for seed <b>${SEED}</b> — bake one with <span class="dim">tools/bake_atlas.py --seed ${SEED}</span>`;
    return;
  }
  byId = new Map(manifest.rooms.map((r) => [r.id, r]));
  // ?room=&facing= jump straight to a room (demo/debug); else start at the entrance.
  const wantRoom = Number(params.get('room'));
  state.room = (params.has('room') && byId.has(wantRoom)) ? wantRoom : manifest.startRoom;
  const wantFacing = (params.get('facing') || '').toUpperCase();
  state.facing = DIRS.includes(wantFacing) ? wantFacing : (firstOpenFacing(state.room) || 'N');
  show();
  preloadAround();
}

function firstOpenFacing(id) {
  const nav = byId.get(id).nav;
  return DIRS.find((d) => nav[d] != null);
}

function show() {
  miss.classList.remove('show');
  img.src = frameURL(state.room, state.facing);
  updateHud();
  drawMinimap();
}

function updateHud() {
  const r = room();
  const exits = DIRS.filter((d) => r.nav[d] != null);
  const ahead = r.nav[state.facing] != null ? `→ room ${r.nav[state.facing]}` : 'wall';
  hud.innerHTML =
    `seed <b>${manifest.seed}</b> · room <b>${r.id}</b>${r.theme ? ` · ${r.theme}` : ''} · ` +
    `facing <b>${state.facing}</b> <span class="dim">(${ahead})</span><br>` +
    `<span class="dim">exits</span> ${exits.join(' ') || '—'}`;
}

function turn(delta) {
  state.facing = DIRS[(DIRS.indexOf(state.facing) + delta + 4) % 4];
  show();
}

function step(forward) {
  const dir = forward ? state.facing : opposite(state.facing);
  const target = room().nav[dir];
  if (target != null) {
    state.room = target;
    show();
    preloadAround();
  } else {
    img.animate(
      [{ filter: 'brightness(1)' }, { filter: 'brightness(0.55)' }, { filter: 'brightness(1)' }],
      { duration: 150 },
    );
  }
}

window.addEventListener('keydown', (e) => {
  const k = e.key.toLowerCase();
  if (k === 'w' || k === 'arrowup') step(true);
  else if (k === 's' || k === 'arrowdown') step(false);
  else if (k === 'a' || k === 'arrowleft') turn(-1);
  else if (k === 'd' || k === 'arrowright') turn(1);
  else return;
  e.preventDefault();
});

// A frame that 404s just hasn't been baked yet — show a hint instead of a broken image.
img.addEventListener('error', () => {
  if (!img.getAttribute('src')) return;
  miss.classList.add('show');
  missDetail.textContent = `room ${state.room}, facing ${state.facing}`;
});

// Warm the browser cache for instant turning/stepping.
function preloadAround() {
  const seen = new Set();
  const pre = (id) => DIRS.forEach((d) => {
    const u = frameURL(id, d);
    if (!seen.has(u)) { seen.add(u); new Image().src = u; }
  });
  pre(state.room);
  const nav = room().nav;
  DIRS.forEach((d) => { if (nav[d] != null) pre(nav[d]); });
}

function drawMinimap() {
  const { tiles, width, height } = manifest;
  const s = Math.max(2, Math.floor(170 / Math.max(width, height)));
  mini.width = width * s; mini.height = height * s;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const t = tiles[y][x];
      mctx.fillStyle = t === 0 ? '#15151c' : t === 2 ? '#caa24a' : '#34343f';
      mctx.fillRect(x * s, y * s, s, s);
    }
  }
  for (const r of manifest.rooms) {                       // every room as a faint node
    mctx.fillStyle = 'rgba(122,160,220,0.45)';
    mctx.fillRect(r.vantage[0] * s, r.vantage[1] * s, s, s);
  }
  const [vx, vy] = room().vantage;                         // the player + facing arrow
  const cx = vx * s + s / 2, cy = vy * s + s / 2;
  mctx.fillStyle = '#e8dcc8';
  mctx.beginPath(); mctx.arc(cx, cy, Math.max(2, s * 0.7), 0, Math.PI * 2); mctx.fill();
  const [dx, dy] = VEC[state.facing];
  mctx.strokeStyle = '#d4a54a'; mctx.lineWidth = Math.max(1.5, s * 0.5);
  mctx.beginPath(); mctx.moveTo(cx, cy); mctx.lineTo(cx + dx * s * 2.4, cy + dy * s * 2.4); mctx.stroke();
}

boot();
