forked from phenom/healer-man
Release Healer Man 0.1.6
This commit is contained in:
1 parent
55cfd43d66
commit
b12fb84859
96 files changed
+10874
-609
No files matched your search
@@ -0,0 +1,319 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { synchronizeOnlineSession } from "../app/onlineAccountClient";
|
||||
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import { useOnlineSessionStore } from "../app/onlineSessionStore";
|
||||
import type {
|
||||
OnlineRelayEvent,
|
||||
OnlineRelayEventInput,
|
||||
OnlineSessionSnapshot,
|
||||
OnlineSharedWorld,
|
||||
} from "../app/onlineSessionTypes";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { equipmentItemsForOwner, PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
|
||||
import { getMobRuntimeTransform } from "./mobRuntimeRegistry";
|
||||
import {
|
||||
configureOnlineSessionRuntime,
|
||||
drainOnlineInteractions,
|
||||
isOnlinePlayerActorId,
|
||||
onlineAccountIdFromActor,
|
||||
onlinePlayerActorId,
|
||||
} from "./onlineSessionRuntime";
|
||||
import {
|
||||
getPartyRuntimePosition,
|
||||
removePartyRuntimePosition,
|
||||
updatePartyRuntimePosition,
|
||||
} from "./partyRuntimeRegistry";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { useGameStore } from "./store";
|
||||
import { useManastormStore } from "./manastormStore";
|
||||
import { advanceManastormSession } from "./manastormSession";
|
||||
import { useWailingEncounterStore } from "./wailingCavernsEncounter";
|
||||
|
||||
const SYNC_INTERVAL_MS = 100;
|
||||
const WORLD_INTERVAL_MS = 200;
|
||||
|
||||
function serializableState<T extends object>(state: T): Readonly<Record<string, unknown>> {
|
||||
return JSON.parse(JSON.stringify(state)) as Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function sharedWorld(): OnlineSharedWorld {
|
||||
const combat = useCombatStore.getState();
|
||||
const party = usePartyStore.getState();
|
||||
const game = useGameStore.getState();
|
||||
const mobTransforms = Object.fromEntries(
|
||||
Object.keys(combat.mobs).flatMap((id) => {
|
||||
const transform = getMobRuntimeTransform(id);
|
||||
return transform ? [[id, transform]] : [];
|
||||
}),
|
||||
);
|
||||
const partyPositions = Object.fromEntries(
|
||||
party.members.flatMap((member) => {
|
||||
const position = getPartyRuntimePosition(member.id);
|
||||
return position ? [[member.id, position]] : [];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
mobs: combat.mobs,
|
||||
mobTransforms,
|
||||
partyMembers: party.members,
|
||||
partyPositions,
|
||||
partyRuntime: {
|
||||
command: party.command,
|
||||
activeMobId: party.activeMobId,
|
||||
objectiveBossId: party.objectiveBossId,
|
||||
memberTargetIds: party.memberTargetIds,
|
||||
},
|
||||
wailing: game.gameMode === "dungeon"
|
||||
? serializableState(useWailingEncounterStore.getState())
|
||||
: null,
|
||||
manastorm: game.gameMode === "manastorm"
|
||||
? serializableState(useManastormStore.getState())
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function applySharedWorld(world: OnlineSharedWorld): void {
|
||||
useCombatStore.setState({ mobs: world.mobs });
|
||||
usePartyStore.setState({
|
||||
members: world.partyMembers,
|
||||
command: world.partyRuntime.command as ReturnType<typeof usePartyStore.getState>["command"],
|
||||
activeMobId: world.partyRuntime.activeMobId,
|
||||
objectiveBossId: world.partyRuntime.objectiveBossId,
|
||||
memberTargetIds: world.partyRuntime.memberTargetIds,
|
||||
});
|
||||
for (const [id, position] of Object.entries(world.partyPositions)) {
|
||||
updatePartyRuntimePosition(id, position[0], position[1], position[2]);
|
||||
}
|
||||
if (world.wailing) useWailingEncounterStore.setState(world.wailing as never);
|
||||
if (world.manastorm) useManastormStore.setState(world.manastorm as never);
|
||||
}
|
||||
|
||||
function eventForRelay(
|
||||
event: ReturnType<typeof useCombatStore.getState>["combatEvents"][number],
|
||||
localAccountId: string,
|
||||
authority: boolean,
|
||||
): OnlineRelayEventInput | null {
|
||||
if (!["damage", "healing", "resurrection"].includes(event.kind)) return null;
|
||||
const combat = useCombatStore.getState();
|
||||
const playerOwnedSummon = combat.summons.some((summon) => (
|
||||
summon.id === event.sourceActorId && summon.ownerActorId === PLAYER_AGGRO_ID
|
||||
));
|
||||
const sourceIsLocal = event.sourceActorId === PLAYER_AGGRO_ID || playerOwnedSummon;
|
||||
const targetsRemotePlayer = isOnlinePlayerActorId(event.targetActorId);
|
||||
if (!sourceIsLocal && !(authority && targetsRemotePlayer)) return null;
|
||||
const localActorId = onlinePlayerActorId(localAccountId);
|
||||
const targetActorId = event.targetActorId === PLAYER_AGGRO_ID
|
||||
? localActorId
|
||||
: event.targetActorId;
|
||||
if (sourceIsLocal && targetActorId === localActorId) return null;
|
||||
return {
|
||||
clientEventId: `combat:${localAccountId}:${event.occurredAt}:${event.id}:${event.kind}`,
|
||||
kind: event.kind as "damage" | "healing" | "resurrection",
|
||||
sourceActorId: sourceIsLocal ? localActorId : event.sourceActorId,
|
||||
targetActorId,
|
||||
abilityId: event.abilityId,
|
||||
school: event.school,
|
||||
rawAmount: event.rawAmount,
|
||||
effectiveAmount: event.effectiveAmount,
|
||||
critical: event.critical,
|
||||
occurredAt: event.occurredAt,
|
||||
};
|
||||
}
|
||||
|
||||
function applyRelayEvent(
|
||||
event: OnlineRelayEvent,
|
||||
snapshot: OnlineSessionSnapshot,
|
||||
applyingRef: React.MutableRefObject<boolean>,
|
||||
): void {
|
||||
if (event.sourceAccountId === snapshot.localAccountId) return;
|
||||
const localActorId = onlinePlayerActorId(snapshot.localAccountId);
|
||||
applyingRef.current = true;
|
||||
try {
|
||||
if (event.kind === "portal" && snapshot.authority) {
|
||||
advanceManastormSession();
|
||||
return;
|
||||
}
|
||||
if (event.kind === "loot" && snapshot.authority && event.targetActorId) {
|
||||
useCombatStore.getState().lootMob(event.targetActorId, event.occurredAt || Date.now());
|
||||
return;
|
||||
}
|
||||
if (event.targetActorId === localActorId) {
|
||||
if (event.kind === "damage") {
|
||||
const sourceLevel = useCombatStore.getState().mobs[event.sourceActorId]?.level;
|
||||
useCombatStore.getState().damagePlayer(
|
||||
event.rawAmount ?? event.effectiveAmount ?? 0,
|
||||
event.school ?? "physical",
|
||||
sourceLevel,
|
||||
);
|
||||
} else if (event.kind === "healing") {
|
||||
useCombatStore.getState().healActor(PLAYER_AGGRO_ID, event.effectiveAmount ?? 0, {
|
||||
sourceActorId: event.sourceActorId,
|
||||
abilityId: event.abilityId ?? undefined,
|
||||
school: event.school ?? undefined,
|
||||
now: event.occurredAt,
|
||||
amountAlreadyModified: true,
|
||||
});
|
||||
} else if (event.kind === "resurrection") {
|
||||
const combat = useCombatStore.getState();
|
||||
combat.revivePlayer((event.effectiveAmount ?? 1) / Math.max(1, combat.maxHealth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!snapshot.authority) return;
|
||||
const partyMember = usePartyStore.getState().members.find((member) => member.id === event.targetActorId);
|
||||
if (partyMember) {
|
||||
if (event.kind === "healing") {
|
||||
useCombatStore.getState().healActor(event.targetActorId, event.effectiveAmount ?? 0, {
|
||||
sourceActorId: event.sourceActorId,
|
||||
abilityId: event.abilityId ?? undefined,
|
||||
school: event.school ?? undefined,
|
||||
now: event.occurredAt,
|
||||
amountAlreadyModified: true,
|
||||
});
|
||||
} else if (event.kind === "resurrection") {
|
||||
useCombatStore.getState().reviveActor(
|
||||
event.targetActorId,
|
||||
(event.effectiveAmount ?? 1) / Math.max(1, partyMember.maxHealth),
|
||||
event.sourceActorId,
|
||||
event.abilityId ?? undefined,
|
||||
event.occurredAt,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const mob = useCombatStore.getState().mobs[event.targetActorId];
|
||||
if (mob && event.kind === "damage") {
|
||||
const sourceMember = snapshot.players.find((player) => player.accountId === event.sourceAccountId);
|
||||
useCombatStore.getState().damageMob(
|
||||
mob.id,
|
||||
event.rawAmount ?? event.effectiveAmount ?? 0,
|
||||
event.occurredAt,
|
||||
{
|
||||
actorId: event.sourceActorId,
|
||||
role: sourceMember?.role ?? "damage",
|
||||
abilityId: event.abilityId ?? undefined,
|
||||
},
|
||||
event.school ?? "physical",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
applyingRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function OnlineSessionBridge() {
|
||||
const session = useShellStore((state) => state.session);
|
||||
const activeActivity = useOnlineGroupStore((state) => state.activeActivity);
|
||||
const group = useOnlineGroupStore((state) => state.snapshot.group);
|
||||
const accept = useOnlineSessionStore((state) => state.accept);
|
||||
const fail = useOnlineSessionStore((state) => state.fail);
|
||||
const reset = useOnlineSessionStore((state) => state.reset);
|
||||
const pendingEventsRef = useRef<OnlineRelayEventInput[]>([]);
|
||||
const applyingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.accessToken || !activeActivity || !group) {
|
||||
configureOnlineSessionRuntime({ active: false, authority: true, localAccountId: null });
|
||||
reset();
|
||||
return undefined;
|
||||
}
|
||||
const localAccountId = session.ownerId;
|
||||
const immediateAuthority = group.leaderAccountId === localAccountId;
|
||||
configureOnlineSessionRuntime({
|
||||
active: true,
|
||||
authority: immediateAuthority,
|
||||
localAccountId,
|
||||
});
|
||||
let disposed = false;
|
||||
let syncing = false;
|
||||
let latestEventId = 0;
|
||||
let lastWorldSentAt = 0;
|
||||
const remotePositionIds = new Set<string>();
|
||||
|
||||
const unsubscribeCombat = useCombatStore.subscribe((state, previous) => {
|
||||
if (applyingRef.current || state.combatEvents === previous.combatEvents) return;
|
||||
const previousKeys = new Set(previous.combatEvents.map((event) => `${event.id}:${event.occurredAt}:${event.kind}`));
|
||||
for (const event of state.combatEvents) {
|
||||
if (previousKeys.has(`${event.id}:${event.occurredAt}:${event.kind}`)) continue;
|
||||
const relay = eventForRelay(event, localAccountId, group.leaderAccountId === localAccountId);
|
||||
if (relay) pendingEventsRef.current.push(relay);
|
||||
}
|
||||
});
|
||||
|
||||
const synchronize = async () => {
|
||||
if (disposed || syncing) return;
|
||||
syncing = true;
|
||||
const combat = useCombatStore.getState();
|
||||
const game = useGameStore.getState();
|
||||
const outgoingEvents = [...pendingEventsRef.current.splice(0), ...drainOnlineInteractions()];
|
||||
const now = Date.now();
|
||||
const publishWorld = group.leaderAccountId === localAccountId && now - lastWorldSentAt >= WORLD_INTERVAL_MS;
|
||||
if (publishWorld) lastWorldSentAt = now;
|
||||
try {
|
||||
const snapshot = await synchronizeOnlineSession(session, {
|
||||
afterEventId: latestEventId,
|
||||
presence: {
|
||||
position: game.playerPosition,
|
||||
yaw: game.cameraYaw,
|
||||
grounded: game.playerGrounded,
|
||||
health: combat.health,
|
||||
maxHealth: combat.maxHealth,
|
||||
resource: combat.resource,
|
||||
maxResource: combat.maxResource,
|
||||
resourceName: combat.resourceName,
|
||||
selectedTargetId: combat.selectedTargetId,
|
||||
activeCast: combat.activeCast,
|
||||
animationEvent: combat.playerAnimationEvent,
|
||||
equipment: equipmentItemsForOwner(
|
||||
combat.equipment,
|
||||
combat.inventory,
|
||||
PLAYER_EQUIPMENT_OWNER_ID,
|
||||
),
|
||||
},
|
||||
...(publishWorld ? { world: sharedWorld() } : {}),
|
||||
...(outgoingEvents.length ? { events: outgoingEvents } : {}),
|
||||
});
|
||||
if (disposed) return;
|
||||
latestEventId = Math.max(latestEventId, snapshot.latestEventId);
|
||||
configureOnlineSessionRuntime({
|
||||
active: true,
|
||||
authority: snapshot.authority,
|
||||
localAccountId,
|
||||
players: snapshot.players,
|
||||
mobTransforms: snapshot.world?.mobTransforms,
|
||||
});
|
||||
for (const player of snapshot.players) {
|
||||
if (player.accountId === localAccountId) continue;
|
||||
const actorId = onlinePlayerActorId(player.accountId);
|
||||
remotePositionIds.add(actorId);
|
||||
updatePartyRuntimePosition(actorId, player.position[0], player.position[1], player.position[2]);
|
||||
}
|
||||
if (!snapshot.authority && snapshot.world) applySharedWorld(snapshot.world);
|
||||
for (const event of snapshot.events) applyRelayEvent(event, snapshot, applyingRef);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
accept(snapshot);
|
||||
} catch (error) {
|
||||
if (outgoingEvents.length) pendingEventsRef.current.unshift(...outgoingEvents);
|
||||
if (!disposed) fail(error instanceof Error ? error.message : "The shared activity lost connection.");
|
||||
} finally {
|
||||
syncing = false;
|
||||
}
|
||||
};
|
||||
|
||||
void synchronize();
|
||||
const timer = window.setInterval(() => void synchronize(), SYNC_INTERVAL_MS);
|
||||
return () => {
|
||||
disposed = true;
|
||||
window.clearInterval(timer);
|
||||
unsubscribeCombat();
|
||||
for (const id of remotePositionIds) removePartyRuntimePosition(id);
|
||||
pendingEventsRef.current = [];
|
||||
configureOnlineSessionRuntime({ active: false, authority: true, localAccountId: null });
|
||||
reset();
|
||||
};
|
||||
}, [accept, activeActivity?.id, fail, group?.id, group?.leaderAccountId, reset, session?.accessToken, session?.ownerId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user