Release Healer Man 0.1.46

This commit is contained in:
phenom committed 2026-09-16 14:30:58 -04:00
1 parent f82daf1c13
commit 9ff574ac0b
73 files changed
+33486 -174

No files matched your search

+70 -36
View File
@@ -23,6 +23,10 @@ import {
onlineSessionOwnsWorldSimulation,
} from "./onlineSessionRuntime";
import { combatSimulationTimerShouldRun } from "./combatSimulationLifecycle";
import {
DUNGEON_SIMULATION_FIXED_STEP_MS,
useDungeonSimulationStore,
} from "./dungeonSimulationStore";
/**
* Connects the pure combat runtime to the active character and persistent
@@ -75,7 +79,7 @@ export function CombatBridge() {
// after Character Select has already replaced the active character.
const shell = useShellStore.getState();
if (shell.activeCharacter?.id !== characterId || shell.session?.ownerId !== ownerId) return;
if (useGameStore.getState().bossTest) return;
if (useGameStore.getState().gmTestMode) return;
// Persisted collections are replaced immutably by combat actions. A
// reference/primitive comparison therefore detects the same changes as
@@ -121,54 +125,80 @@ export function CombatBridge() {
let previous = performance.now();
let timer: number | null = null;
const advanceOneTick = (delta: number, epochNow: number, spectator: boolean) => {
const game = useGameStore.getState();
const simulation = useDungeonSimulationStore.getState();
const combat = useCombatStore.getState();
combat.setPlayerPosition(game.playerPosition);
combat.tick(delta, epochNow);
if (
onlineSessionOwnsWorldSimulation()
&& game.gameMode === "dungeon"
&& (!spectator || simulation.mode === "full-combat")
) {
useCombatStore.getState().engageNearbyMobs(epochNow, game.playerPosition);
}
if (onlineSessionOwnsWorldSimulation()) {
if (!spectator || simulation.mode === "full-combat") {
useCombatStore.getState().advanceMobCombat(epochNow, game.playerPosition);
}
advanceDungeonPartyCombat(epochNow);
}
useCombatStore.getState().synchronizeActors();
if (onlineSessionOwnsWorldSimulation() && game.gameMode === "dungeon") {
const mobs = useCombatStore.getState().mobs;
const combatEvents = deriveDungeonEncounterCombatEvents(
encounterMobCursor.current,
mobs,
epochNow,
);
encounterMobCursor.current = combatEvents.cursor;
for (const event of combatEvents.events) dispatchActiveDungeonEntityEvent(event);
const registeredEntityIds = Object.keys(mobs);
advanceDungeonEncounters(game.activeDungeonId, {
kind: "tick",
now: epochNow,
facts: {
registeredEntityIds,
deadEntityIds: registeredEntityIds.filter((id) => mobs[id].dead),
},
});
}
};
const advance = () => {
const now = performance.now();
const delta = Math.min(0.25, Math.max(0, (now - previous) / 1_000));
const wallDeltaMs = Math.min(1_000, Math.max(0, now - previous));
const delta = Math.min(0.25, wallDeltaMs / 1_000);
previous = now;
const simulation = useDungeonSimulationStore.getState();
if (simulation.sessionMode === "spectate-replay") {
simulation.consumeTicks(wallDeltaMs);
return;
}
if (simulation.sessionMode === "spectate-live") {
const assetStatus = useGameStore.getState().assetStatus;
if (assetStatus !== "ready" && assetStatus !== "proxy") return;
for (const tickNow of simulation.consumeTicks(wallDeltaMs)) {
advanceOneTick(DUNGEON_SIMULATION_FIXED_STEP_MS / 1_000, tickNow, true);
}
return;
}
const game = useGameStore.getState();
const sharedOnline = onlineSessionIsActive();
if ((!game.paused || sharedOnline) && (!game.companionOpen || sharedOnline)) {
const epochNow = Date.now();
const combat = useCombatStore.getState();
combat.setPlayerPosition(game.playerPosition);
combat.tick(delta, epochNow);
if (onlineSessionOwnsWorldSimulation() && game.gameMode === "dungeon") {
useCombatStore.getState().engageNearbyMobs(epochNow, game.playerPosition);
}
if (onlineSessionOwnsWorldSimulation()) {
useCombatStore.getState().advanceMobCombat(epochNow, game.playerPosition);
advanceDungeonPartyCombat(epochNow);
}
useCombatStore.getState().synchronizeActors();
if (onlineSessionOwnsWorldSimulation() && game.gameMode === "dungeon") {
const mobs = useCombatStore.getState().mobs;
const combatEvents = deriveDungeonEncounterCombatEvents(
encounterMobCursor.current,
mobs,
epochNow,
);
encounterMobCursor.current = combatEvents.cursor;
for (const event of combatEvents.events) dispatchActiveDungeonEntityEvent(event);
const registeredEntityIds = Object.keys(mobs);
advanceDungeonEncounters(game.activeDungeonId, {
kind: "tick",
now: epochNow,
facts: {
registeredEntityIds,
deadEntityIds: registeredEntityIds.filter((id) => mobs[id].dead),
},
});
}
advanceOneTick(delta, Date.now(), false);
}
};
const reconcileTimer = () => {
const game = useGameStore.getState();
const simulation = useDungeonSimulationStore.getState();
const shouldRun = combatSimulationTimerShouldRun({
sharedOnline: onlineSessionIsActive(),
paused: game.paused,
companionOpen: game.companionOpen,
documentHidden: document.hidden,
paused: simulation.sessionMode === "play" ? game.paused : false,
companionOpen: simulation.sessionMode === "play" ? game.companionOpen : false,
documentHidden: simulation.sessionMode === "play" ? document.hidden : false,
});
if (shouldRun && timer === null) {
// Reset the clock on resume so a long pause/background period cannot
@@ -193,6 +223,9 @@ export function CombatBridge() {
// store notification gives the bridge a reactive point at which to
// reconcile while preserving continuous shared-online simulation.
const unsubscribeOnlineSession = useOnlineSessionStore.subscribe(reconcileTimer);
const unsubscribeSimulation = useDungeonSimulationStore.subscribe((state, previousState) => {
if (state.sessionMode !== previousState.sessionMode || state.runState !== previousState.runState) reconcileTimer();
});
document.addEventListener("visibilitychange", reconcileTimer);
reconcileTimer();
@@ -200,6 +233,7 @@ export function CombatBridge() {
if (timer !== null) window.clearInterval(timer);
unsubscribeGame();
unsubscribeOnlineSession();
unsubscribeSimulation();
document.removeEventListener("visibilitychange", reconcileTimer);
};
}, []);