107 lines
5.3 KiB
TypeScript
107 lines
5.3 KiB
TypeScript
import type { AppScreen } from "../frontend/types";
|
|
import type { FrontendSnapshot } from "../frontend/store";
|
|
import { useFrontendStore } from "../frontend/store";
|
|
import { emitControllerToken, setExternalControllerMovement, type ControllerMovement, type ControllerTokenEvent } from "../input/controller";
|
|
import { getGameSnapshot, type GameSnapshot, useGameStore } from "../game/store";
|
|
import type { AbilityId, BottomTab, MemberId } from "../game/types";
|
|
import type { BossId, HealerClassId } from "../game/types";
|
|
import type { GameModeId, GameSettings, SaveSlotId } from "../frontend/types";
|
|
|
|
const CHANNEL_NAME = "i-want-to-heal:thor-dual-screen:v1";
|
|
|
|
export type GameCommand =
|
|
| { name: "startEncounter" }
|
|
| { name: "restart" }
|
|
| { name: "castAbility"; abilityId: AbilityId }
|
|
| { name: "selectMember"; memberId: MemberId }
|
|
| { name: "cycleMember"; direction: 1 | -1 }
|
|
| { name: "setActiveTab"; tab: BottomTab }
|
|
| { name: "selectItem"; itemId: string }
|
|
| { name: "setPaused"; paused: boolean }
|
|
| { name: "setPauseSelection"; selection: "resume" | "exit" };
|
|
|
|
export type FrontendCommand =
|
|
| { name: "signIn"; username: string; password: string }
|
|
| { name: "createAccount"; username: string; password: string }
|
|
| { name: "continueOffline" }
|
|
| { name: "signOut" }
|
|
| { name: "navigate"; screen: AppScreen }
|
|
| { name: "selectSlot"; slotId: SaveSlotId }
|
|
| { name: "createSlot"; slotId: SaveSlotId; hunterName: string }
|
|
| { name: "playSlot"; slotId: SaveSlotId }
|
|
| { name: "deleteSlot"; slotId: SaveSlotId }
|
|
| { name: "copySlot"; sourceId: SaveSlotId; targetId: SaveSlotId }
|
|
| { name: "uploadSlot"; slotId: SaveSlotId }
|
|
| { name: "downloadSlot"; slotId: SaveSlotId }
|
|
| { name: "selectMode"; mode: GameModeId }
|
|
| { name: "selectBoss"; bossId: BossId }
|
|
| { name: "selectHealerClass"; classId: HealerClassId }
|
|
| { name: "updateSetting"; key: keyof GameSettings; value: GameSettings[keyof GameSettings] }
|
|
| { name: "launchGame"; bossIds: readonly BossId[] };
|
|
|
|
export const DUAL_SCREEN_LAUNCH_EVENT = "iwt:dual-screen-launch-game";
|
|
|
|
export type DualScreenMessage =
|
|
| { type: "app-state"; screen: AppScreen; hunterName: string | null; notice: string; frontend?: FrontendSnapshot; game?: GameSnapshot }
|
|
| { type: "controller-token"; id: string; event: ControllerTokenEvent }
|
|
| { type: "controller-echo"; id: string; event: ControllerTokenEvent }
|
|
| { type: "controller-motion"; movement: ControllerMovement }
|
|
| { type: "game-command"; command: GameCommand }
|
|
| { type: "frontend-command"; command: FrontendCommand }
|
|
| { type: "authoritative-ready" }
|
|
| { type: "companion-ready" }
|
|
| { type: "companion-closing" };
|
|
|
|
export function createDualScreenChannel() {
|
|
return typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(CHANNEL_NAME);
|
|
}
|
|
|
|
export function executeGameCommand(command: GameCommand) {
|
|
const game = useGameStore.getState();
|
|
switch (command.name) {
|
|
case "startEncounter": game.startEncounter(); break;
|
|
case "restart": game.restart(); break;
|
|
case "castAbility": game.castAbility(command.abilityId); break;
|
|
case "selectMember": game.selectMember(command.memberId); break;
|
|
case "cycleMember": game.cycleMember(command.direction); break;
|
|
case "setActiveTab": game.setActiveTab(command.tab); break;
|
|
case "selectItem": game.selectItem(command.itemId); break;
|
|
case "setPaused": game.setPaused(command.paused); break;
|
|
case "setPauseSelection": game.setPauseSelection(command.selection); break;
|
|
}
|
|
}
|
|
|
|
export function executeFrontendCommand(command: FrontendCommand) {
|
|
const frontend = useFrontendStore.getState();
|
|
switch (command.name) {
|
|
case "signIn": void frontend.signIn(command.username, command.password); break;
|
|
case "createAccount": void frontend.createAccount(command.username, command.password); break;
|
|
case "continueOffline": frontend.continueOffline(); break;
|
|
case "signOut": frontend.signOut(); break;
|
|
case "navigate": frontend.navigate(command.screen); break;
|
|
case "selectSlot": frontend.selectSlot(command.slotId); break;
|
|
case "createSlot": frontend.createSlot(command.slotId, command.hunterName); break;
|
|
case "playSlot": frontend.playSlot(command.slotId); break;
|
|
case "deleteSlot": frontend.deleteSlot(command.slotId); break;
|
|
case "copySlot": frontend.copySlot(command.sourceId, command.targetId); break;
|
|
case "uploadSlot": frontend.uploadSlot(command.slotId); break;
|
|
case "downloadSlot": frontend.downloadSlot(command.slotId); break;
|
|
case "selectMode": frontend.selectMode(command.mode); break;
|
|
case "selectBoss": frontend.selectBoss(command.bossId); break;
|
|
case "selectHealerClass": frontend.selectHealerClass(command.classId); break;
|
|
case "updateSetting": frontend.updateSetting(command.key, command.value); break;
|
|
case "launchGame": window.dispatchEvent(new CustomEvent(DUAL_SCREEN_LAUNCH_EVENT, { detail: command.bossIds })); break;
|
|
}
|
|
}
|
|
|
|
export function receiveAuthoritativeMessage(message: DualScreenMessage) {
|
|
if (message.type === "controller-token") emitControllerToken(message.event);
|
|
if (message.type === "controller-motion") setExternalControllerMovement(message.movement);
|
|
if (message.type === "game-command") executeGameCommand(message.command);
|
|
if (message.type === "frontend-command") executeFrontendCommand(message.command);
|
|
}
|
|
|
|
export function currentGameSnapshot() {
|
|
return getGameSnapshot();
|
|
}
|