Release Healer Man 0.1.9
This commit is contained in:
1 parent
c245a199b6
commit
d060296ade
74 files changed
+5901
-334
No files matched your search
+84
-60
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user