Release v0.1.4 2026-07-12
This commit is contained in:
@@ -2,7 +2,7 @@ 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 GameState, useGameStore } from "../game/store";
|
||||
import type { AbilityId, BottomTab, MemberId, RunBuffId } from "../game/types";
|
||||
import type { BossId, HealerClassId } from "../game/types";
|
||||
import type { GameModeId, GameSettings, SaveSlotId } from "../frontend/types";
|
||||
@@ -54,7 +54,7 @@ export type FrontendCommand =
|
||||
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: "app-state"; screen: AppScreen; hunterName: string | null; notice: string; frontend?: FrontendSnapshot; game?: Partial<BottomGameSnapshot> }
|
||||
| { type: "controller-token"; id: string; event: ControllerTokenEvent }
|
||||
| { type: "controller-echo"; id: string; event: ControllerTokenEvent }
|
||||
| { type: "controller-motion"; movement: ControllerMovement }
|
||||
@@ -123,6 +123,93 @@ export function receiveAuthoritativeMessage(message: DualScreenMessage) {
|
||||
if (message.type === "frontend-command") executeFrontendCommand(message.command);
|
||||
}
|
||||
|
||||
export function currentGameSnapshot() {
|
||||
return getGameSnapshot();
|
||||
/** State the lower display actually renders. Renderer-only and progression data stay local to the authoritative screen. */
|
||||
export type BottomGameSnapshot = Pick<GameState,
|
||||
| "bossId"
|
||||
| "paused"
|
||||
| "healerClassId"
|
||||
| "phase"
|
||||
| "round"
|
||||
| "time"
|
||||
| "party"
|
||||
| "boss"
|
||||
| "additionalBosses"
|
||||
| "partyPositions"
|
||||
| "bossMotion"
|
||||
| "partyCombat"
|
||||
| "mana"
|
||||
| "maxMana"
|
||||
| "selectedMemberId"
|
||||
| "cooldowns"
|
||||
| "globalCooldownUntil"
|
||||
| "activeTab"
|
||||
| "selectedItemId"
|
||||
| "inventory"
|
||||
| "playerPosition"
|
||||
| "activeCast"
|
||||
| "barrier"
|
||||
>;
|
||||
|
||||
const BOTTOM_GAME_SNAPSHOT_KEYS: readonly (keyof BottomGameSnapshot)[] = [
|
||||
"bossId", "paused", "healerClassId", "phase", "round", "time", "party", "boss", "additionalBosses",
|
||||
"partyPositions", "bossMotion", "partyCombat", "mana", "maxMana", "selectedMemberId", "cooldowns",
|
||||
"globalCooldownUntil", "activeTab", "selectedItemId", "inventory", "playerPosition", "activeCast", "barrier",
|
||||
];
|
||||
|
||||
function structurallyEqual(left: unknown, right: unknown): boolean {
|
||||
if (Object.is(left, right)) return true;
|
||||
if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
|
||||
if (Array.isArray(left) || Array.isArray(right)) {
|
||||
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
||||
for (let index = 0; index < left.length; index += 1) {
|
||||
if (!structurallyEqual(left[index], right[index])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const leftRecord = left as Record<string, unknown>;
|
||||
const rightRecord = right as Record<string, unknown>;
|
||||
const keys = Object.keys(leftRecord);
|
||||
if (keys.length !== Object.keys(rightRecord).length) return false;
|
||||
return keys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key) && structurallyEqual(leftRecord[key], rightRecord[key]));
|
||||
}
|
||||
|
||||
export function currentBottomGameSnapshot(): BottomGameSnapshot {
|
||||
const state = useGameStore.getState();
|
||||
return {
|
||||
bossId: state.bossId,
|
||||
paused: state.paused,
|
||||
healerClassId: state.healerClassId,
|
||||
phase: state.phase,
|
||||
round: state.round,
|
||||
time: state.time,
|
||||
party: state.party,
|
||||
boss: state.boss,
|
||||
additionalBosses: state.additionalBosses,
|
||||
partyPositions: state.partyPositions,
|
||||
bossMotion: state.bossMotion,
|
||||
partyCombat: state.partyCombat,
|
||||
mana: state.mana,
|
||||
maxMana: state.maxMana,
|
||||
selectedMemberId: state.selectedMemberId,
|
||||
cooldowns: state.cooldowns,
|
||||
globalCooldownUntil: state.globalCooldownUntil,
|
||||
activeTab: state.activeTab,
|
||||
selectedItemId: state.selectedItemId,
|
||||
inventory: state.inventory,
|
||||
playerPosition: state.playerPosition,
|
||||
activeCast: state.activeCast,
|
||||
barrier: state.barrier,
|
||||
};
|
||||
}
|
||||
|
||||
export function diffBottomGameSnapshot(
|
||||
previous: BottomGameSnapshot | undefined,
|
||||
next: BottomGameSnapshot,
|
||||
): Partial<BottomGameSnapshot> {
|
||||
if (!previous) return next;
|
||||
const changes: Partial<BottomGameSnapshot> = {};
|
||||
for (const key of BOTTOM_GAME_SNAPSHOT_KEYS) {
|
||||
if (!structurallyEqual(previous[key], next[key])) Object.assign(changes, { [key]: next[key] });
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user