197 lines
9.0 KiB
TypeScript
197 lines
9.0 KiB
TypeScript
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]);
|
|
}
|