forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
48a678dcc5
commit
9c5fb4c614
18 files changed
+397
-46
No files matched your search
@@ -103,4 +103,15 @@ describe("aura strip visibility", () => {
|
||||
|
||||
expect(groupAurasForDisplay([first, second])).toEqual([second]);
|
||||
});
|
||||
|
||||
it("counts separate applications of the same debuff while collapsing different aura components", () => {
|
||||
const poison = activeAura(aura({ id: "poison", name: "Poison", icon: "/poison.png" }));
|
||||
const otherSource = { ...poison, instanceId: "poison-2", sourceId: "rogue-2", appliedAt: 2_000 };
|
||||
const thirdSource = { ...poison, instanceId: "poison-3", sourceId: "rogue-3", appliedAt: 3_000 };
|
||||
expect(groupAurasForDisplay([poison, otherSource, thirdSource])).toEqual([
|
||||
{ ...thirdSource, stacks: 3 },
|
||||
]);
|
||||
const secondaryComponent = { ...poison, definition: aura({ id: "poison-secondary", name: "Poison", icon: "/poison.png" }) };
|
||||
expect(groupAurasForDisplay([poison, secondaryComponent])[0]?.stacks).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { abilityById } from "../game/abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { formatTimedEffectTimer, timedEffectRemainingMs } from "../game/timedEffectDisplay";
|
||||
import { formatTimedEffectTimer, groupTimedEffectsForDisplay, timedEffectRemainingMs } from "../game/timedEffectDisplay";
|
||||
import { AuraStrip } from "./AuraStrip";
|
||||
import {
|
||||
auraDisplaysInPlayerStatusPanel,
|
||||
@@ -33,19 +33,18 @@ export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
const activeAuras = groupAurasForDisplay(auras.filter((aura) => (
|
||||
auraDisplaysInPlayerStatusPanel(aura, now)
|
||||
)));
|
||||
const activeEffects = effects.filter((effect) => (
|
||||
const activeEffects = groupTimedEffectsForDisplay(effects.filter((effect) => (
|
||||
PLAYER_EFFECT_TARGETS.includes(effect.targetId)
|
||||
&& timedEffectRemainingMs(effect, now) > 0
|
||||
));
|
||||
)), now);
|
||||
const shieldActive = shield > 0 && (shieldExpiresAt <= 0 || shieldExpiresAt > now);
|
||||
const buffCount = activeAuras.filter((aura) => aura.definition.disposition === "buff").length
|
||||
+ activeEffects.filter((effect) => effect.kind === "hot").length
|
||||
+ activeEffects.filter(({ effect }) => effect.kind === "hot").length
|
||||
+ (shieldActive ? 1 : 0);
|
||||
const debuffCount = activeAuras.filter((aura) => aura.definition.disposition === "debuff").length
|
||||
+ activeEffects.filter((effect) => effect.kind === "dot").length;
|
||||
+ activeEffects.filter(({ effect }) => effect.kind === "dot").length;
|
||||
const activeInspectionIds = new Set([
|
||||
...activeAuras.map((aura) => aura.instanceId),
|
||||
...activeEffects.map((effect) => effect.id),
|
||||
...activeEffects.map(({ effect }) => effect.id),
|
||||
...(shieldActive ? ["player-absorb-shield"] : []),
|
||||
]);
|
||||
const inspectedIsActive = inspected ? activeInspectionIds.has(inspected.id) : true;
|
||||
@@ -79,8 +78,9 @@ export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
: {}),
|
||||
} satisfies StatusEffectInspection;
|
||||
}
|
||||
const effect = activeEffects.find((candidate) => candidate.id === inspected.id);
|
||||
if (effect) {
|
||||
const displayedEffect = activeEffects.find(({ effect }) => effect.id === inspected.id);
|
||||
if (displayedEffect) {
|
||||
const { effect, stacks } = displayedEffect;
|
||||
const ability = abilityById(effect.sourceAbilityId);
|
||||
return {
|
||||
...inspected,
|
||||
@@ -88,6 +88,7 @@ export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
disposition: effect.kind === "hot" ? "buff" : "debuff",
|
||||
description: timedEffectDescription(effect.kind, ability?.description),
|
||||
remainingLabel: `${formatTimedEffectTimer(timedEffectRemainingMs(effect, now))} remaining`,
|
||||
stacks,
|
||||
} satisfies StatusEffectInspection;
|
||||
}
|
||||
return inspected;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { abilityById } from "../game/abilityCatalog";
|
||||
import { useCombatStore, type TimedCombatEffect } from "../game/combatStore";
|
||||
import {
|
||||
formatTimedEffectTimer,
|
||||
timedEffectRemainingMs,
|
||||
groupTimedEffectsForDisplay,
|
||||
} from "../game/timedEffectDisplay";
|
||||
import {
|
||||
timedEffectDescription,
|
||||
@@ -41,20 +41,14 @@ function LiveTimedEffectStrip({
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const visibleEffects = effects
|
||||
.map((effect) => ({
|
||||
effect,
|
||||
ability: abilityById(effect.sourceAbilityId),
|
||||
remainingMs: timedEffectRemainingMs(effect, now),
|
||||
}))
|
||||
.filter(({ remainingMs }) => remainingMs > 0)
|
||||
.sort((left, right) => left.remainingMs - right.remainingMs);
|
||||
const visibleEffects = groupTimedEffectsForDisplay(effects, now);
|
||||
|
||||
if (!visibleEffects.length) return null;
|
||||
|
||||
return (
|
||||
<span className={`timed-effect-strip ${className}`.trim()} aria-label="Periodic effects">
|
||||
{visibleEffects.map(({ effect, ability, remainingMs }) => {
|
||||
{visibleEffects.map(({ effect, stacks, remainingMs }) => {
|
||||
const ability = abilityById(effect.sourceAbilityId);
|
||||
const label = ability?.name ?? effect.sourceAbilityId;
|
||||
const timer = formatTimedEffectTimer(remainingMs);
|
||||
const disposition = effect.kind === "hot" ? "buff" : "debuff";
|
||||
@@ -64,16 +58,16 @@ function LiveTimedEffectStrip({
|
||||
disposition,
|
||||
description: timedEffectDescription(effect.kind, ability?.description),
|
||||
remainingLabel: `${timer} remaining`,
|
||||
stacks: 1,
|
||||
stacks,
|
||||
};
|
||||
return (
|
||||
<span
|
||||
key={effect.id}
|
||||
className={`timed-effect timed-effect--${effect.kind} ${onInspect ? "timed-effect--inspectable" : ""}`.trim()}
|
||||
tabIndex={onInspect ? 0 : undefined}
|
||||
aria-label={`${label}, ${effect.kind === "hot" ? "healing" : "damage"} over time, ${timer} remaining`}
|
||||
aria-label={`${label}, ${effect.kind === "hot" ? "healing" : "damage"} over time${stacks > 1 ? `, ${stacks} stacks` : ""}, ${timer} remaining`}
|
||||
aria-describedby={onInspect ? tooltipId : undefined}
|
||||
title={onInspect ? undefined : `${label} · ${timer} remaining`}
|
||||
title={onInspect ? undefined : `${label}${stacks > 1 ? ` · ${stacks} stacks` : ""} · ${timer} remaining`}
|
||||
onPointerEnter={onInspect ? () => onInspect(inspection) : undefined}
|
||||
onPointerLeave={onInspect ? (event) => {
|
||||
if (event.currentTarget !== document.activeElement) onInspect(null);
|
||||
@@ -84,6 +78,7 @@ function LiveTimedEffectStrip({
|
||||
{ability?.icon
|
||||
? <img src={ability.icon} alt="" draggable={false} />
|
||||
: <span className="timed-effect__fallback" aria-hidden="true">{effect.kind === "hot" ? "+" : "!"}</span>}
|
||||
{stacks > 1 ? <b className="timed-effect__stacks" aria-hidden="true">{stacks}</b> : null}
|
||||
<b className="timed-effect__timer" aria-hidden="true">{timer}</b>
|
||||
</span>
|
||||
);
|
||||
|
||||
+12
-10
@@ -30,24 +30,26 @@ function auraDisplayExpiry(aura: ActiveAura): number {
|
||||
|
||||
/** Collapses a spell's multiple DBC aura effects and repeated sources into one HUD icon. */
|
||||
export function groupAurasForDisplay(auras: readonly ActiveAura[]): ActiveAura[] {
|
||||
const grouped = new Map<string, ActiveAura>();
|
||||
const grouped = new Map<string, { representative: ActiveAura; stacksByDefinition: Map<string, number> }>();
|
||||
for (const aura of auras) {
|
||||
const key = auraDisplayGroupKey(aura);
|
||||
const existing = grouped.get(key);
|
||||
if (!existing) {
|
||||
grouped.set(key, aura);
|
||||
grouped.set(key, { representative: aura, stacksByDefinition: new Map([[aura.definition.id, aura.stacks]]) });
|
||||
continue;
|
||||
}
|
||||
const representative = auraDisplayExpiry(aura) > auraDisplayExpiry(existing)
|
||||
|| (auraDisplayExpiry(aura) === auraDisplayExpiry(existing) && aura.appliedAt > existing.appliedAt)
|
||||
const representative = auraDisplayExpiry(aura) > auraDisplayExpiry(existing.representative)
|
||||
|| (auraDisplayExpiry(aura) === auraDisplayExpiry(existing.representative) && aura.appliedAt > existing.representative.appliedAt)
|
||||
? aura
|
||||
: existing;
|
||||
const stacks = Math.max(existing.stacks, aura.stacks);
|
||||
grouped.set(key, representative.stacks === stacks
|
||||
? representative
|
||||
: { ...representative, stacks });
|
||||
: existing.representative;
|
||||
existing.stacksByDefinition.set(aura.definition.id,
|
||||
(existing.stacksByDefinition.get(aura.definition.id) ?? 0) + aura.stacks);
|
||||
existing.representative = representative;
|
||||
}
|
||||
return [...grouped.values()];
|
||||
return [...grouped.values()].map(({ representative, stacksByDefinition }) => {
|
||||
const stacks = Math.max(...stacksByDefinition.values());
|
||||
return representative.stacks === stacks ? representative : { ...representative, stacks };
|
||||
});
|
||||
}
|
||||
|
||||
/** Permanent and one-minute-or-longer auras are persistent party-frame clutter. */
|
||||
|
||||
Reference in new issue
Block a user