Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 21:50:50 -04:00
1 parent fdb6739f8d
commit f90ba7d989
41 files changed
+1671 -118

No files matched your search

+92 -2
View File
@@ -1,4 +1,4 @@
import { useState, type CSSProperties } from "react";
import { useEffect, useRef, useState, type CSSProperties } from "react";
import { classById, isRomClassId, raceById } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { useCombatStore } from "../game/combatStore";
@@ -21,6 +21,12 @@ import { partyResurrectionBlockedByBoss, resurrectDeadPartyMembers } from "../ga
import { ResurrectionDestination, useResurrectionCheckpoints } from "./ResurrectionDestination";
import { useGameStore } from "../game/store";
import { useHudSurface } from "./HudSurface";
import {
PARTY_HEAL_POPUP_LIFETIME_MS,
addedPartyHealPopups,
appendPartyHealPopups,
type PartyHealPopup,
} from "./partyHealPopups";
const PARTY_COMMANDS: readonly {
command: PartyCommand;
@@ -109,10 +115,85 @@ function healthPercent(current: number, maximum: number): number {
return Math.max(0, Math.min(100, current / maximum * 100));
}
function MemberFrame({ member, selected, onSelect }: {
function HealPopups({ popups, targetId }: {
popups: readonly PartyHealPopup[];
targetId: string;
}) {
const targetPopups = popups.filter((popup) => popup.targetActorId === targetId);
if (targetPopups.length === 0) return null;
return (
<span className="party-frame__heal-popups" aria-hidden="true">
{targetPopups.map((popup) => (
<span
key={popup.key}
className={`party-frame__heal-popup ${popup.critical ? "is-critical" : ""}`}
style={{ "--party-heal-x": `${popup.horizontalOffset}px` } as CSSProperties}
>+{popup.amount}</span>
))}
</span>
);
}
function usePartyHealPopups(enabled: boolean): readonly PartyHealPopup[] {
const [popups, setPopups] = useState<PartyHealPopup[]>([]);
const timers = useRef(new Map<string, number>());
useEffect(() => {
const clearPopups = () => {
for (const timer of timers.current.values()) window.clearTimeout(timer);
timers.current.clear();
setPopups([]);
};
if (!enabled) {
clearPopups();
return undefined;
}
const unsubscribe = useCombatStore.subscribe((state, previous) => {
const inCombat = Object.values(state.mobs).some((mob) => (
mob.engaged && !mob.dead && !mob.despawned
));
const wasInCombat = Object.values(previous.mobs).some((mob) => (
mob.engaged && !mob.dead && !mob.despawned
));
if (!inCombat) {
if (wasInCombat) clearPopups();
return;
}
const visibleTargetIds = new Set([
PLAYER_AGGRO_ID,
...usePartyStore.getState().members.map((member) => member.id),
...(useOnlineSessionStore.getState().snapshot?.players ?? []).map((player) => (
onlinePlayerActorId(player.accountId)
)),
]);
const added = addedPartyHealPopups(previous.combatEvents, state.combatEvents, true)
.filter((popup) => visibleTargetIds.has(popup.targetActorId));
if (added.length === 0) return;
setPopups((current) => appendPartyHealPopups(current, added));
for (const popup of added) {
const existing = timers.current.get(popup.key);
if (existing !== undefined) window.clearTimeout(existing);
timers.current.set(popup.key, window.setTimeout(() => {
timers.current.delete(popup.key);
setPopups((current) => current.filter((candidate) => candidate.key !== popup.key));
}, PARTY_HEAL_POPUP_LIFETIME_MS));
}
});
return () => {
unsubscribe();
for (const timer of timers.current.values()) window.clearTimeout(timer);
timers.current.clear();
};
}, [enabled]);
return popups;
}
function MemberFrame({ member, selected, onSelect, healPopups }: {
member: PartyMember;
selected: boolean;
onSelect: () => void;
healPopups: readonly PartyHealPopup[];
}) {
const characterClass = classById(member.classId);
const race = raceById(member.raceId);
@@ -163,6 +244,7 @@ function MemberFrame({ member, selected, onSelect }: {
highlightDispellableDebuffs
/>
</span>
<HealPopups popups={healPopups} targetId={member.id} />
</button>
);
}
@@ -172,11 +254,13 @@ function OnlineMemberFrame({
player,
selected,
onSelect,
healPopups,
}: {
member: OnlineGroupMember;
player: OnlineSessionPlayer | null;
selected: boolean;
onSelect: () => void;
healPopups: readonly PartyHealPopup[];
}) {
const character = member.character;
if (!character) return null;
@@ -207,6 +291,7 @@ function OnlineMemberFrame({
</>
) : <span className="party-frame__online-status">Connecting…</span>}
</span>
<HealPopups popups={healPopups} targetId={onlinePlayerActorId(member.accountId)} />
</button>
);
}
@@ -221,6 +306,8 @@ export function PartyFrames() {
const playerResource = useCombatStore((state) => state.resource);
const playerMaxResource = useCombatStore((state) => state.maxResource);
const playerResourceName = useCombatStore((state) => state.resourceName);
const showHealPopups = useCombatStore((state) => state.settings.showFloatingCombatText);
const healPopups = usePartyHealPopups(showHealPopups);
const playerAuraAbsorb = useCombatStore((state) => state.auras.reduce((total, aura) => (
aura.targetId === PLAYER_AGGRO_ID
? total + aura.absorbs.reduce((sum, pool) => sum + pool.remaining, 0)
@@ -312,6 +399,7 @@ export function PartyFrames() {
highlightDispellableDebuffs
/>
</span>
<HealPopups popups={healPopups} targetId={PLAYER_AGGRO_ID} />
</button>
{onlineMembers.map((member) => {
const actorId = onlinePlayerActorId(member.accountId);
@@ -322,6 +410,7 @@ export function PartyFrames() {
player={onlinePlayersByAccount.get(member.accountId) ?? null}
selected={selectedOnlineActorId === actorId}
onSelect={() => selectOnlineMember(member.accountId)}
healPopups={healPopups}
/>
);
})}
@@ -331,6 +420,7 @@ export function PartyFrames() {
member={member}
selected={member.id === selectedMemberId}
onSelect={() => selectMember(member.id)}
healPopups={healPopups}
/>
))}
{gameMode === "dungeon" && members.some((member) => member.health <= 0) ? (