import { useEffect, useRef } from "react"; import { subscribeControllerToken } from "../input/controller"; import { ABILITY_ORDER } from "./data"; import { isRunBuffInputLocked, useGameStore } from "./store"; import { ABILITY_BY_CONTROLLER_BUTTON } from "./controllerBindings"; import { cycleBottomTab } from "./bottomTabs"; import { resolveRpgFocusCommand } from "./rpgRoguelike/uiModel"; import { getDisplaySurface } from "../platform/displayRouting"; import { isSingleScreenLayout } from "../platform/displayLayout"; function tacticalOverlayOwnsInput() { const store = useGameStore.getState(); if (!isSingleScreenLayout() || getDisplaySurface() !== "bottom") return false; if (store.paused || store.phase === "intermission") return false; if (store.runMode === "rpg-roguelike" && rpgInputIsGated()) return false; return !(store.phase === "victory" && store.runMode === "rogue-trials" && store.round === 5 && !store.endlessMode); } function cycleRunBuff(direction: 1 | -1) { const store = useGameStore.getState(); if (store.draftBuffIds.length === 0) return; const currentIndex = store.selectedRunBuffId ? store.draftBuffIds.indexOf(store.selectedRunBuffId) : -1; const nextIndex = (Math.max(0, currentIndex) + direction + store.draftBuffIds.length) % store.draftBuffIds.length; store.setSelectedRunBuff(store.draftBuffIds[nextIndex]); } function rpgInputIsGated() { const run = useGameStore.getState().rpgRun; return Boolean(run && run.phase !== "challenge-active" && run.phase !== "boss-combat"); } function activateRpgFocus(onExit?: () => void) { const store = useGameStore.getState(); if (!store.rpgRun) return; const command = resolveRpgFocusCommand(store.rpgRun, store.rpgFocusId); if (!command) return; if (command.type === "run-action") store.dispatchRpgAction(command.action); else if (command.type === "restart-run") store.restart(); else onExit?.(); } export function useActionBindings(enabled = true, onExit?: () => void) { const exitRef = useRef(onExit); exitRef.current = onExit; useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (!enabled) return; if (event.defaultPrevented) return; if (event.repeat) return; const store = useGameStore.getState(); const key = event.key.toLowerCase(); if (tacticalOverlayOwnsInput()) return; if (store.paused) { if (["escape", "arrowup", "arrowdown", "enter"].includes(key)) event.preventDefault(); if (key === "escape") store.setPaused(false); if (key === "arrowup") store.setPauseSelection("resume"); if (key === "arrowdown") store.setPauseSelection("exit"); if (key === "enter") { if (store.pauseSelection === "resume") store.setPaused(false); else exitRef.current?.(); } return; } if (store.runMode === "rpg-roguelike" && store.rpgRun && rpgInputIsGated()) { if (["arrowleft", "arrowup", "arrowright", "arrowdown", "enter", "escape"].includes(key)) event.preventDefault(); if (key === "arrowleft") store.moveRpgFocus("left"); if (key === "arrowright") store.moveRpgFocus("right"); if (key === "arrowup") store.moveRpgFocus("up"); if (key === "arrowdown") store.moveRpgFocus("down"); if (key === "enter") activateRpgFocus(exitRef.current); if (key === "escape") exitRef.current?.(); return; } if (store.phase === "intermission") { if (["arrowleft", "arrowup", "arrowright", "arrowdown", "enter"].includes(key)) event.preventDefault(); if (isRunBuffInputLocked(store)) return; if (key === "arrowleft" || key === "arrowup") cycleRunBuff(-1); if (key === "arrowright" || key === "arrowdown") cycleRunBuff(1); if (key === "enter") { if (store.selectedRunBuffId) store.chooseRunBuff(store.selectedRunBuffId); else store.continueRoguelikeRound(); } if (key === "escape") exitRef.current?.(); return; } if (store.phase === "victory" && store.runMode === "rogue-trials" && store.round === 5 && !store.endlessMode) { if (["arrowleft", "arrowup", "arrowright", "arrowdown", "enter"].includes(key)) event.preventDefault(); if (key === "arrowleft" || key === "arrowup") store.setEndlessChoiceSelection("continue"); if (key === "arrowright" || key === "arrowdown") store.setEndlessChoiceSelection("quit"); if (key === "enter") { if (store.endlessChoiceSelection === "continue") store.startRogueTrialsEndless(); else exitRef.current?.(); } if (key === "escape") exitRef.current?.(); return; } const numberIndex = Number(event.key) - 1; if (numberIndex >= 0 && numberIndex < ABILITY_ORDER.length) { store.castAbility(ABILITY_ORDER[numberIndex]); return; } switch (key) { case "q": store.cycleMember(-1); break; case "e": store.cycleMember(1); break; case "m": store.setActiveTab(store.activeTab === "map" ? "combat" : "map"); break; case "i": if (store.runMode !== "hockey-healing-pvp") { store.setActiveTab(store.activeTab === "pack" ? "combat" : "pack"); } break; case "p": if (store.runMode === "hockey-healing-pvp") { store.setActiveTab(store.activeTab === "pvp" ? "combat" : "pvp"); } break; case "enter": if (store.phase === "briefing") store.startEncounter(); if (store.phase === "victory" || store.phase === "defeat") store.restart(); break; case "escape": if (store.phase === "combat") store.setPaused(true); else exitRef.current?.(); break; } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [enabled]); useEffect(() => subscribeControllerToken(({ token, repeat }) => { if (!enabled) return; const store = useGameStore.getState(); if (isSingleScreenLayout() && token === "Button8") return; if (tacticalOverlayOwnsInput()) return; if (store.paused) { if (token === "Button12" || token === "Axis1-") store.setPauseSelection("resume"); if (token === "Button13" || token === "Axis1+") store.setPauseSelection("exit"); if (repeat) return; if (token === "Button1" || token === "Button9") store.setPaused(false); if (token === "Button0") { if (store.pauseSelection === "resume") store.setPaused(false); else exitRef.current?.(); } return; } if (store.runMode === "rpg-roguelike" && store.rpgRun && rpgInputIsGated()) { if (token === "Button12" || token === "Axis1-") store.moveRpgFocus("up"); if (token === "Button13" || token === "Axis1+") store.moveRpgFocus("down"); if (token === "Button14" || token === "Axis0-") store.moveRpgFocus("left"); if (token === "Button15" || token === "Axis0+") store.moveRpgFocus("right"); if (!repeat && token === "Button0") activateRpgFocus(exitRef.current); if (!repeat && token === "Button1") exitRef.current?.(); return; } if (store.phase === "intermission") { if (isRunBuffInputLocked(store)) return; if (["Button12", "Button14", "Axis0-", "Axis1-"].includes(token)) cycleRunBuff(-1); if (["Button13", "Button15", "Axis0+", "Axis1+"].includes(token)) cycleRunBuff(1); if (!repeat && token === "Button0") { if (store.selectedRunBuffId) store.chooseRunBuff(store.selectedRunBuffId); else store.continueRoguelikeRound(); } if (!repeat && token === "Button1") exitRef.current?.(); return; } if (store.phase === "victory" && store.runMode === "rogue-trials" && store.round === 5 && !store.endlessMode) { if (["Button12", "Button14", "Axis0-", "Axis1-"].includes(token)) store.setEndlessChoiceSelection("continue"); if (["Button13", "Button15", "Axis0+", "Axis1+"].includes(token)) store.setEndlessChoiceSelection("quit"); if (!repeat && token === "Button0") { if (store.endlessChoiceSelection === "continue") store.startRogueTrialsEndless(); else exitRef.current?.(); } if (!repeat && token === "Button1") exitRef.current?.(); return; } if (repeat) return; if (token.startsWith("Button")) { const ability = ABILITY_BY_CONTROLLER_BUTTON[Number(token.slice("Button".length))]; if (ability && store.phase === "combat") store.castAbility(ability); } if (token === "Button12") store.cycleMember(-1); if (token === "Button13") store.cycleMember(1); if (token === "Button8") store.setActiveTab(cycleBottomTab(store.activeTab, store.runMode)); if (token === "Button9" || (token === "Button0" && store.phase !== "combat")) { if (store.phase === "briefing") store.startEncounter(); else if (store.phase === "victory" || store.phase === "defeat") store.restart(); else if (store.phase === "combat") store.setPaused(true); } }), [enabled]); }