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