Files
i-want-to-heal-mmo/src/game/useGameLoop.ts
T
2026-07-19 13:58:47 -04:00

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