Release v0.1.3 2026-07-11
This commit is contained in:
+49
-14
@@ -5,6 +5,7 @@ import { FrontEnd } from "./components/FrontEnd";
|
||||
import { useActiveHunter, useFrontendStore } from "./frontend/store";
|
||||
import { useGameStore } from "./game/store";
|
||||
import type { BossId } from "./game/types";
|
||||
import type { DifficultySlug } from "./game/progression/loot";
|
||||
import { useActionBindings, useGameLoop } from "./game/useGameLoop";
|
||||
import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen";
|
||||
import { DUAL_SCREEN_LAUNCH_EVENT } from "./platform/dualScreenSync";
|
||||
@@ -32,25 +33,35 @@ export default function App() {
|
||||
const touchActiveSave = useFrontendStore((state) => state.touchActiveSave);
|
||||
const updateActiveHealerInventory = useFrontendStore((state) => state.updateActiveHealerInventory);
|
||||
const recordBossVictory = useFrontendStore((state) => state.recordBossVictory);
|
||||
const phase = useGameStore((state) => state.phase);
|
||||
const boss = useGameStore((state) => state.boss);
|
||||
const additionalBosses = useGameStore((state) => state.additionalBosses);
|
||||
const victoryRecorded = useRef(false);
|
||||
const clearRecentRewards = useFrontendStore((state) => state.clearRecentRewards);
|
||||
const rewardedBossInstances = useRef(new Set<string>());
|
||||
const screenRef = useRef(screen);
|
||||
screenRef.current = screen;
|
||||
const leaveGame = useCallback(() => {
|
||||
updateActiveHealerInventory(useGameStore.getState().inventory);
|
||||
touchActiveSave();
|
||||
navigate("home");
|
||||
}, [navigate, touchActiveSave, updateActiveHealerInventory]);
|
||||
const launchGame = useCallback((bossIds: readonly BossId[]) => {
|
||||
const launchGame = useCallback((bossIds: readonly BossId[], requestedDifficultySlug?: DifficultySlug) => {
|
||||
if (!hunter) return;
|
||||
const progress = hunter.healers[hunter.activeClassId];
|
||||
useGameStore.getState().configureHealer(hunter.activeClassId, hunter.hunterName, progress.inventory, bossIds);
|
||||
const runMode = useFrontendStore.getState().selectedMode === "roguelike-pve" ? "roguelike" : "encounter";
|
||||
const launchDifficulty = runMode === "roguelike"
|
||||
? "initiate"
|
||||
: requestedDifficultySlug ?? useFrontendStore.getState().selectedDifficultySlug;
|
||||
rewardedBossInstances.current.clear();
|
||||
clearRecentRewards();
|
||||
useGameStore.getState().configureHealer(hunter.activeClassId, hunter.hunterName, progress.inventory, bossIds, runMode, hunter.gearProgress, launchDifficulty);
|
||||
touchActiveSave();
|
||||
navigate("game");
|
||||
}, [hunter, navigate, touchActiveSave]);
|
||||
}, [clearRecentRewards, hunter, navigate, touchActiveSave]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDualScreenLaunch = (event: Event) => launchGame((event as CustomEvent<readonly BossId[]>).detail);
|
||||
const onDualScreenLaunch = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ bossIds: readonly BossId[]; difficultySlug?: DifficultySlug } | readonly BossId[]>).detail;
|
||||
if ("bossIds" in detail) launchGame(detail.bossIds, detail.difficultySlug);
|
||||
else launchGame(detail);
|
||||
};
|
||||
window.addEventListener(DUAL_SCREEN_LAUNCH_EVENT, onDualScreenLaunch);
|
||||
return () => window.removeEventListener(DUAL_SCREEN_LAUNCH_EVENT, onDualScreenLaunch);
|
||||
}, [launchGame]);
|
||||
@@ -63,12 +74,36 @@ export default function App() {
|
||||
}, [settings.largeText, settings.reducedMotion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (phase === "combat") victoryRecorded.current = false;
|
||||
if (screen === "game" && phase === "victory" && !victoryRecorded.current) {
|
||||
victoryRecorded.current = true;
|
||||
for (const bossName of [boss.name, ...additionalBosses.map((entry) => entry.boss.name)]) recordBossVictory(bossName);
|
||||
}
|
||||
}, [additionalBosses, boss.name, phase, recordBossVictory, screen]);
|
||||
return useGameStore.subscribe((state, previousState) => {
|
||||
const startedFreshEncounter = state.phase === "briefing" && previousState.phase !== "briefing"
|
||||
|| previousState.phase === "intermission" && state.phase === "combat";
|
||||
if (state.phase === "briefing" || previousState.phase === "intermission" && state.phase === "combat") {
|
||||
rewardedBossInstances.current.clear();
|
||||
}
|
||||
if (startedFreshEncounter) clearRecentRewards();
|
||||
if (screenRef.current !== "game") return;
|
||||
const bossCount = 1 + state.additionalBosses.length;
|
||||
if (state.boss.hp <= 0 && previousState.boss.hp > 0) {
|
||||
const primaryInstanceId = `boss-0-${state.boss.id}`;
|
||||
if (!rewardedBossInstances.current.has(primaryInstanceId)) {
|
||||
rewardedBossInstances.current.add(primaryInstanceId);
|
||||
const defeatedBefore = (state.round - 1) * bossCount;
|
||||
const rewardDifficulty = state.runMode === "roguelike" && defeatedBefore >= 5 ? "veteran" : state.difficultySlug;
|
||||
recordBossVictory(state.boss.id, rewardDifficulty);
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < state.additionalBosses.length; index += 1) {
|
||||
const entry = state.additionalBosses[index];
|
||||
const previous = previousState.additionalBosses[index];
|
||||
const justDefeated = entry.boss.hp <= 0 && (!previous || previous.instanceId !== entry.instanceId || previous.boss.hp > 0);
|
||||
if (!justDefeated || rewardedBossInstances.current.has(entry.instanceId)) continue;
|
||||
rewardedBossInstances.current.add(entry.instanceId);
|
||||
const defeatedBefore = (state.round - 1) * bossCount + index + 1;
|
||||
const rewardDifficulty = state.runMode === "roguelike" && defeatedBefore >= 5 ? "veteran" : state.difficultySlug;
|
||||
recordBossVictory(entry.boss.id, rewardDifficulty);
|
||||
}
|
||||
});
|
||||
}, [clearRecentRewards, recordBossVictory]);
|
||||
|
||||
return (
|
||||
<main className="prototype-shell">
|
||||
|
||||
Reference in New Issue
Block a user