Release v0.1.4 2026-07-12

This commit is contained in:
Warren H
2026-07-12 13:49:46 -04:00
parent b48b3a4f8f
commit bef71d391a
803 changed files with 3800 additions and 358322 deletions
+91 -4
View File
@@ -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;
}