core/input.js

import { game, STATE } from "./game.js";
import { RANGED_WEAPON, VERTICAL } from "./flags.js";
import { ROWS, COLUMNS } from "./board.js";
import {
  NORTH,
  SOUTH,
  EAST,
  WEST,
  NORTHWEST,
  NORTHEAST,
  SOUTHWEST,
  SOUTHEAST,
  UP,
  DOWN,
} from "./direction.js";
import { findPathInDirection, Targeting } from "../pieces/agents/targeting.js";

/**
 * Key → { action, dir? } mapping — matches original Java InputManager key bindings.
 *
 * Movement:       arrow keys / vi keys (hjkl + yubn) / numpad
 * Pickup:         g (or p)
 * Drop:           d
 * Use:            Enter
 * Throw:          t followed by a direction key (two-step)
 * Fire (ranged):  f followed by a direction key (two-step), OR shift+direction (one-step)
 *                 Shift+arrow, shift+vi (HJKLYUBN), or ctrl+numpad
 * Vertical:       z (game detects UP vs DOWN from terrain)
 * Bag select-up:  a
 * Bag select-down: s
 * First weapon:   w
 * Empty-handed:   e
 * Reorder up:     c (or <)
 * Reorder down:   v (or >)
 */
const KEY_MAP = new Map([
  // Cardinal movement — arrow keys
  ["ArrowUp", { action: "move", dir: NORTH }],
  ["ArrowDown", { action: "move", dir: SOUTH }],
  ["ArrowRight", { action: "move", dir: EAST }],
  ["ArrowLeft", { action: "move", dir: WEST }],
  // Vi keys
  ["k", { action: "move", dir: NORTH }],
  ["j", { action: "move", dir: SOUTH }],
  ["l", { action: "move", dir: EAST }],
  ["h", { action: "move", dir: WEST }],
  ["y", { action: "move", dir: NORTHWEST }],
  ["u", { action: "move", dir: NORTHEAST }],
  ["b", { action: "move", dir: SOUTHWEST }],
  ["n", { action: "move", dir: SOUTHEAST }],
  // Numpad
  ["8", { action: "move", dir: NORTH }],
  ["2", { action: "move", dir: SOUTH }],
  ["6", { action: "move", dir: EAST }],
  ["4", { action: "move", dir: WEST }],
  ["7", { action: "move", dir: NORTHWEST }],
  ["9", { action: "move", dir: NORTHEAST }],
  ["1", { action: "move", dir: SOUTHWEST }],
  ["3", { action: "move", dir: SOUTHEAST }],
  // Item actions
  ["g", { action: "pickup" }],
  ["p", { action: "pickup" }],
  ["d", { action: "drop" }],
  ["Enter", { action: "use" }],
  [" ", { action: "use" }],
  // Throw: t followed by a direction key (handled specially)
  ["t", { action: "throw-mode" }],
  // Ranged fire: f+dir (two-step) or shift+dir (one-step, handled in keydown)
  ["f", { action: "fire-mode" }],
  // Shift+vi keys fire instantly in that direction
  ["K", { action: "fire", dir: NORTH }],
  ["J", { action: "fire", dir: SOUTH }],
  ["L", { action: "fire", dir: EAST }],
  ["H", { action: "fire", dir: WEST }],
  ["Y", { action: "fire", dir: NORTHWEST }],
  ["U", { action: "fire", dir: NORTHEAST }],
  ["B", { action: "fire", dir: SOUTHWEST }],
  ["N", { action: "fire", dir: SOUTHEAST }],
  // Ctrl+numpad fires instantly (numpad with control key)
  ["Control+8", { action: "fire", dir: NORTH }],
  ["Control+2", { action: "fire", dir: SOUTH }],
  ["Control+6", { action: "fire", dir: EAST }],
  ["Control+4", { action: "fire", dir: WEST }],
  ["Control+7", { action: "fire", dir: NORTHWEST }],
  ["Control+9", { action: "fire", dir: NORTHEAST }],
  ["Control+1", { action: "fire", dir: SOUTHWEST }],
  ["Control+3", { action: "fire", dir: SOUTHEAST }],
  // Vertical movement (single key — direction detected from terrain)
  ["z", { action: "vertical" }],
  // Bag management
  ["a", { action: "cycle-up" }],
  ["s", { action: "cycle-down" }],
  ["w", { action: "select-weapon" }],
  ["e", { action: "select-empty" }],
  ["c", { action: "reorder-up" }],
  ["v", { action: "reorder-down" }],
  ["<", { action: "reorder-up" }],
  [">", { action: "reorder-down" }],
]);

/** Direction keys used after throw/fire-mode */
const DIR_KEYS = new Map([
  ["ArrowUp", NORTH],
  ["ArrowDown", SOUTH],
  ["ArrowRight", EAST],
  ["ArrowLeft", WEST],
  ["k", NORTH],
  ["j", SOUTH],
  ["l", EAST],
  ["h", WEST],
  ["y", NORTHWEST],
  ["u", NORTHEAST],
  ["b", SOUTHWEST],
  ["n", SOUTHEAST],
  ["8", NORTH],
  ["2", SOUTH],
  ["6", EAST],
  ["4", WEST],
  ["7", NORTHWEST],
  ["9", NORTHEAST],
  ["1", SOUTHWEST],
  ["3", SOUTHEAST],
]);

let _pendingAction = null; // "throw" | "fire" | null

/**
 * Wire keyboard input to the game controller.
 * @param {HTMLElement} target  element to listen on (typically document or #board)
 */
export function initInput(target = document) {
  target.addEventListener("keydown", (e) => {
    if (game.state !== STATE.PLAYING) return;

    // Don't intercept keys while any modal dialog is open — let the dialog
    // (and any focused input inside it) handle keyboard input unobstructed.
    if (document.querySelector("dialog[open]")) return;

    // Shift+arrow keys fire instantly (arrow key values are the same shifted)
    if (e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
      const arrowDir = {
        ArrowUp: NORTH,
        ArrowDown: SOUTH,
        ArrowRight: EAST,
        ArrowLeft: WEST,
      }[e.key];
      if (arrowDir) {
        e.preventDefault();
        _pendingAction = null;
        game.handleInput("fire", arrowDir);
        return;
      }
    }

    // Pending direction for throw/fire
    if (_pendingAction) {
      const dir = DIR_KEYS.get(e.key);
      if (dir) {
        e.preventDefault();
        const action = _pendingAction;
        _pendingAction = null;
        game.handleInput(action, dir);
        return;
      }
      // Any other key cancels
      _pendingAction = null;
    }

    // Regular digit keys (not numpad) → numbered item pickup.
    // Numpad digits share event.key "1"-"9" but have event.code "Numpad1" etc.
    if (!e.ctrlKey && !e.shiftKey && !e.altKey && /^Digit[0-9]$/.test(e.code)) {
      const digit = parseInt(e.key);
      const index = digit === 0 ? 9 : digit - 1; // '1'→0, '2'→1, …, '0'→9
      e.preventDefault();
      game.handleInput("pickup-index", index);
      return;
    }

    // Build lookup key: ctrl+numpad uses "Control+digit" entries
    const lookupKey =
      e.ctrlKey && !e.shiftKey && !e.altKey ? `Control+${e.key}` : e.key;
    const binding = KEY_MAP.get(lookupKey);
    if (!binding) return;

    e.preventDefault();

    if (binding.action === "throw-mode") {
      _pendingAction = "throw";
      return;
    }
    if (binding.action === "fire-mode") {
      _pendingAction = "fire";
      return;
    }

    game.handleInput(binding.action, binding.dir ?? null);
  });
}

/** Lookup table: `"${xDelta},${yDelta}"` → Direction constant */
const DIR_BY_DELTA = new Map([
  ["0,-1", NORTH],
  ["0,1", SOUTH],
  ["1,0", EAST],
  ["-1,0", WEST],
  ["-1,-1", NORTHWEST],
  ["1,-1", NORTHEAST],
  ["-1,1", SOUTHWEST],
  ["1,1", SOUTHEAST],
]);

/**
 * Wire mouse clicks on the board and inventory to game actions.
 * Board click rules:
 *   - Click on a different cell: move one step toward it (or fire with Shift).
 *   - Click on the player's own cell: pick up the topmost item.
 * Inventory click: select that item in the bag.
 *
 * @param {HTMLElement} boardEl
 */
export function initMouseInput(boardEl) {
  // ── Board clicks ────────────────────────────────────────────────────────
  boardEl.addEventListener("click", (e) => {
    if (game.state !== STATE.PLAYING) return;
    if (document.querySelector("dialog[open]")) return;

    const span =
      e.target.closest?.(".cell") ??
      (e.target.classList.contains("cell") ? e.target : null);
    if (!span) return;

    const tx = parseInt(span.dataset.x, 10);
    const ty = parseInt(span.dataset.y, 10);
    if (isNaN(tx) || isNaN(ty)) return;

    const px = game.board?.playerX;
    const py = game.board?.playerY;
    if (px == null || py == null) return;

    e.preventDefault();

    if (tx === px && ty === py) {
      // Clicked the player's own cell.
      // If there are items, pick up.
      // If on VERTICAL terrain (stairs/cave), use it.
      // Otherwise, if on a map edge, walk off it.
      if (!game.board?.getCurrentCell()?.isBagEmpty) {
        game.handleInput("pickup", null);
        return;
      }
      if (game.board?.getCurrentCell()?.terrain?.is(VERTICAL)) {
        game.handleInput("vertical", null);
        return;
      }
      // Determine edge direction (north/south take priority over east/west)
      const edgeDir =
        py === 0
          ? NORTH
          : py === ROWS - 1
            ? SOUTH
            : px === 0
              ? WEST
              : px === COLUMNS - 1
                ? EAST
                : null;
      if (edgeDir) game.handleInput("move", edgeDir);
      return;
    }

    // Compute direction toward the target
    const dx = tx - px;
    const dy = ty - py;
    const adx = Math.abs(dx);
    const ady = Math.abs(dy);
    const normDir = DIR_BY_DELTA.get(`${Math.sign(dx)},${Math.sign(dy)}`);
    if (!normDir) return;

    let action;
    if (e.shiftKey) {
      const selected = game.player?.bag?.getSelected();
      action = selected?.is(RANGED_WEAPON) ? "fire" : "throw";
    } else {
      action = "move";
    }

    let dir = normDir;
    // For move actions when the target isn't the immediate adjacent cell,
    // use pathfinding to navigate around obstacles (mirrors Java InputManager).
    if (action === "move" && (adx > 1 || ady > 1)) {
      const fromCell = game.board?.getCurrentCell();
      const targetCell = game.board?.getCellAt(tx, ty);
      const player = game.player;
      if (fromCell && targetCell && player) {
        const found = findPathInDirection(
          game.board,
          fromCell,
          player,
          targetCell,
          normDir,
          new Targeting(),
        );
        if (found) dir = found;
      }
    }

    game.handleInput(action, dir);
  });

  // ── Inventory clicks ─────────────────────────────────────────────────────
  // Delegate on the list container so it works even after the list is re-rendered.
  const inventoryList = document.getElementById("inventory-list");
  if (inventoryList) {
    inventoryList.addEventListener("click", (e) => {
      if (game.state !== STATE.PLAYING) return;
      const li = e.target.closest("li");
      if (!li) return;
      const index = Array.from(inventoryList.children).indexOf(li);
      if (index < 0) return;
      game.player?.bag?.select(index);
    });
  }
}