Update Healer Man server
This commit is contained in:
1 parent
fdb6739f8d
commit
f90ba7d989
41 files changed
+1671
-118
No files matched your search
+92
-2
@@ -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) ? (
|
||||
|
||||
Reference in new issue
Block a user