Release Healer Man 0.1.9

This commit is contained in:
phenom committed 2026-08-30 14:16:56 -04:00
1 parent c245a199b6
commit d060296ade
74 files changed
+5901 -334

No files matched your search

+84 -60
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef } from "react";
import { useShellStore } from "../app/shellStore";
import { useOnlineSessionStore } from "../app/onlineSessionStore";
import { useCombatStore } from "./combatStore";
import { advanceDungeonPartyCombat } from "./partyRuntime";
import { useGameStore } from "./store";
@@ -10,6 +11,7 @@ import {
onlineSessionIsActive,
onlineSessionOwnsWorldSimulation,
} from "./onlineSessionRuntime";
import { combatSimulationTimerShouldRun } from "./combatSimulationLifecycle";
/**
* Connects the pure combat runtime to the active character and persistent
@@ -42,73 +44,55 @@ export function CombatBridge() {
useEffect(() => {
if (!character) return;
let lastKey = `${character.level}:${character.experience}:${JSON.stringify(character.talentRanks)}`;
const unsubscribe = useCombatStore.subscribe((state) => {
const nextKey = `${state.level}:${state.xp}:${JSON.stringify(state.talentRanks)}`;
if (nextKey === lastKey) return;
lastKey = nextKey;
// Kills and talent changes are infrequent, so persist immediately. This
// also guarantees a fast return to Character Select cannot outrun a
// deferred save and lose the last level or talent point.
useShellStore.getState().saveActiveProgression(state.level, state.xp, state.talentRanks);
});
return unsubscribe;
}, [character?.id]);
const characterId = character.id;
const unsubscribe = useCombatStore.subscribe((state, previousState) => {
// Save APIs intentionally target the active character. Avoid persisting a
// final combat notification during the render-to-effect-cleanup window
// after Character Select has already replaced the active character.
const shell = useShellStore.getState();
if (shell.activeCharacter?.id !== characterId) return;
useEffect(() => {
if (!character) return;
let lastKey = JSON.stringify(useCombatStore.getState().actionBindings);
const unsubscribe = useCombatStore.subscribe((state) => {
const nextKey = JSON.stringify(state.actionBindings);
if (nextKey === lastKey) return;
lastKey = nextKey;
// Manastorm's local spell-selection substitute temporarily rewrites the
// base controls. Never persist that run-only overlay to normal dungeons.
if (useGameStore.getState().gameMode === "manastorm") return;
useShellStore.getState().saveActiveActionBindings(state.actionBindings);
});
return unsubscribe;
}, [character?.id]);
// Persisted collections are replaced immutably by combat actions. A
// reference/primitive comparison therefore detects the same changes as
// serializing each collection, without doing five full-store callbacks
// and JSON.stringify work for every 100 ms simulation notification.
if (
state.level !== previousState.level
|| state.xp !== previousState.xp
|| state.talentRanks !== previousState.talentRanks
) {
// Kills and talent changes are infrequent, so persist immediately. This
// also guarantees a fast return to Character Select cannot outrun a
// deferred save and lose the last level or talent point.
shell.saveActiveProgression(state.level, state.xp, state.talentRanks);
}
useEffect(() => {
if (!character) return;
let lastInventory = JSON.stringify(character.inventory);
const unsubscribe = useCombatStore.subscribe((state) => {
const nextInventory = JSON.stringify(state.inventory);
if (nextInventory === lastInventory) return;
lastInventory = nextInventory;
useShellStore.getState().saveActiveInventory(state.inventory);
});
return unsubscribe;
}, [character?.id]);
if (state.actionBindings !== previousState.actionBindings) {
// Manastorm's local spell-selection substitute temporarily rewrites the
// base controls. Never persist that run-only overlay to normal dungeons.
if (useGameStore.getState().gameMode !== "manastorm") {
shell.saveActiveActionBindings(state.actionBindings);
}
}
useEffect(() => {
if (!character) return;
let lastEquipment = JSON.stringify(character.equipment ?? {});
const unsubscribe = useCombatStore.subscribe((state) => {
const nextEquipment = JSON.stringify(state.equipment);
if (nextEquipment === lastEquipment) return;
lastEquipment = nextEquipment;
useShellStore.getState().saveActiveEquipment(state.equipment);
});
return unsubscribe;
}, [character?.id]);
useEffect(() => {
if (!character) return;
let lastSettings = JSON.stringify(useCombatStore.getState().settings);
const unsubscribe = useCombatStore.subscribe((state) => {
const nextSettings = JSON.stringify(state.settings);
if (nextSettings === lastSettings) return;
lastSettings = nextSettings;
useShellStore.getState().saveActiveSettings(state.settings);
if (state.inventory !== previousState.inventory) {
shell.saveActiveInventory(state.inventory);
}
if (state.equipment !== previousState.equipment) {
shell.saveActiveEquipment(state.equipment);
}
if (state.settings !== previousState.settings) {
shell.saveActiveSettings(state.settings);
}
});
return unsubscribe;
}, [character?.id]);
useEffect(() => {
let previous = performance.now();
const timer = window.setInterval(() => {
let timer: number | null = null;
const advance = () => {
const now = performance.now();
const delta = Math.min(0.25, Math.max(0, (now - previous) / 1_000));
previous = now;
@@ -138,8 +122,48 @@ export function CombatBridge() {
);
}
}
}, 100);
return () => window.clearInterval(timer);
};
const reconcileTimer = () => {
const game = useGameStore.getState();
const shouldRun = combatSimulationTimerShouldRun({
sharedOnline: onlineSessionIsActive(),
paused: game.paused,
companionOpen: game.companionOpen,
documentHidden: document.hidden,
});
if (shouldRun && timer === null) {
// Reset the clock on resume so a long pause/background period cannot
// leak into the first simulation delta.
previous = performance.now();
timer = window.setInterval(advance, 100);
} else if (!shouldRun && timer !== null) {
window.clearInterval(timer);
timer = null;
}
};
const unsubscribeGame = useGameStore.subscribe((state, previousState) => {
if (
state.paused !== previousState.paused
|| state.companionOpen !== previousState.companionOpen
) {
reconcileTimer();
}
});
// Online runtime configuration is intentionally module-local; the session
// store notification gives the bridge a reactive point at which to
// reconcile while preserving continuous shared-online simulation.
const unsubscribeOnlineSession = useOnlineSessionStore.subscribe(reconcileTimer);
document.addEventListener("visibilitychange", reconcileTimer);
reconcileTimer();
return () => {
if (timer !== null) window.clearInterval(timer);
unsubscribeGame();
unsubscribeOnlineSession();
document.removeEventListener("visibilitychange", reconcileTimer);
};
}, []);
return null;