Release Healer Man 0.1.9
This commit is contained in:
1 parent
c245a199b6
commit
d060296ade
74 files changed
+5901
-334
No files matched your search
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { abilityById } from "../game/abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
||||
import type { ActiveAura } from "../game/combatAuras";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { usePartyStore } from "../game/partyStore";
|
||||
import { formatTimedEffectTimer, timedEffectRemainingMs } from "../game/timedEffectDisplay";
|
||||
import {
|
||||
AuraStrip,
|
||||
auraDefinitionDisplaysInStrip,
|
||||
auraDefinitionIsLongTerm,
|
||||
auraDescription,
|
||||
auraDisplayName,
|
||||
} from "./AuraStrip";
|
||||
import { TimedEffectStrip } from "./TimedEffectStrip";
|
||||
import {
|
||||
timedEffectDescription,
|
||||
type StatusEffectInspection,
|
||||
} from "./statusEffectInspection";
|
||||
|
||||
const PLAYER_EFFECT_TARGETS: readonly (string | null)[] = [null, PLAYER_AGGRO_ID];
|
||||
const TOOLTIP_ID = "player-status-effect-tooltip";
|
||||
|
||||
function statusLabel(disposition: StatusEffectInspection["disposition"]): string {
|
||||
return disposition === "buff" ? "Buff" : "Debuff";
|
||||
}
|
||||
|
||||
export function auraDisplaysInPlayerStatusPanel(
|
||||
aura: ActiveAura,
|
||||
now: number,
|
||||
partyTargetIds: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const belongsInPanel = aura.targetId === PLAYER_AGGRO_ID
|
||||
|| (partyTargetIds.has(aura.targetId) && auraDefinitionIsLongTerm(aura.definition));
|
||||
return belongsInPanel
|
||||
&& auraDefinitionDisplaysInStrip(aura.definition)
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now);
|
||||
}
|
||||
|
||||
export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
const auras = useCombatStore((state) => state.auras);
|
||||
const effects = useCombatStore((state) => state.effects);
|
||||
const shield = useCombatStore((state) => state.shield);
|
||||
const shieldExpiresAt = useCombatStore((state) => state.shieldExpiresAt);
|
||||
const partyMembers = usePartyStore((state) => state.members);
|
||||
const [inspected, setInspected] = useState<StatusEffectInspection | null>(null);
|
||||
|
||||
const partyAuraTargetIds = partyMembers.map((member) => member.id);
|
||||
const partyAuraTargetIdSet = new Set(partyAuraTargetIds);
|
||||
const partyAuraTargetLabels = Object.fromEntries(
|
||||
partyMembers.map((member) => [member.id, member.name]),
|
||||
);
|
||||
const activeAuras = auras.filter((aura) => (
|
||||
auraDisplaysInPlayerStatusPanel(aura, now, partyAuraTargetIdSet)
|
||||
));
|
||||
const activeEffects = effects.filter((effect) => (
|
||||
PLAYER_EFFECT_TARGETS.includes(effect.targetId)
|
||||
&& timedEffectRemainingMs(effect, now) > 0
|
||||
));
|
||||
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
|
||||
+ (shieldActive ? 1 : 0);
|
||||
const debuffCount = activeAuras.filter((aura) => aura.definition.disposition === "debuff").length
|
||||
+ activeEffects.filter((effect) => effect.kind === "dot").length;
|
||||
const activeInspectionIds = new Set([
|
||||
...activeAuras.map((aura) => aura.instanceId),
|
||||
...activeEffects.map((effect) => effect.id),
|
||||
...(shieldActive ? ["player-absorb-shield"] : []),
|
||||
]);
|
||||
const inspectedIsActive = inspected ? activeInspectionIds.has(inspected.id) : true;
|
||||
const shieldInspection: StatusEffectInspection = {
|
||||
id: "player-absorb-shield",
|
||||
name: "Absorb Shield",
|
||||
disposition: "buff",
|
||||
description: `Absorbs up to ${Math.ceil(shield)} incoming damage before health is lost.`,
|
||||
remainingLabel: shieldExpiresAt > now
|
||||
? `${formatTimedEffectTimer(shieldExpiresAt - now)} remaining`
|
||||
: "Until depleted",
|
||||
stacks: 1,
|
||||
};
|
||||
const liveInspection = inspected && inspectedIsActive
|
||||
? (() => {
|
||||
if (inspected.id === shieldInspection.id) return shieldInspection;
|
||||
const aura = activeAuras.find((candidate) => candidate.instanceId === inspected.id);
|
||||
if (aura) {
|
||||
const remaining = aura.expiresAt === null
|
||||
? "Permanent"
|
||||
: `${formatTimedEffectTimer(Math.max(0, aura.expiresAt - now))} remaining`;
|
||||
return {
|
||||
...inspected,
|
||||
name: auraDisplayName(aura.definition),
|
||||
disposition: aura.definition.disposition,
|
||||
description: auraDescription(aura),
|
||||
remainingLabel: remaining,
|
||||
stacks: aura.stacks,
|
||||
...(aura.definition.dispelCategory
|
||||
? { dispelCategory: aura.definition.dispelCategory }
|
||||
: {}),
|
||||
} satisfies StatusEffectInspection;
|
||||
}
|
||||
const effect = activeEffects.find((candidate) => candidate.id === inspected.id);
|
||||
if (effect) {
|
||||
const ability = abilityById(effect.sourceAbilityId);
|
||||
return {
|
||||
...inspected,
|
||||
name: ability?.name ?? effect.sourceAbilityId,
|
||||
disposition: effect.kind === "hot" ? "buff" : "debuff",
|
||||
description: timedEffectDescription(effect.kind, ability?.description),
|
||||
remainingLabel: `${formatTimedEffectTimer(timedEffectRemainingMs(effect, now))} remaining`,
|
||||
} satisfies StatusEffectInspection;
|
||||
}
|
||||
return inspected;
|
||||
})()
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (inspected && !inspectedIsActive) setInspected(null);
|
||||
}, [inspected, inspectedIsActive]);
|
||||
|
||||
return (
|
||||
<section className="player-status-panel" aria-label="Active buffs and debuffs">
|
||||
<div
|
||||
className="player-status-panel__section player-status-panel__section--buffs"
|
||||
aria-label={`${buffCount} active ${buffCount === 1 ? "buff" : "buffs"}`}
|
||||
>
|
||||
<header><strong>Buffs</strong><b>{buffCount}</b></header>
|
||||
<div className="player-status-panel__icons">
|
||||
{shieldActive ? (
|
||||
<span
|
||||
className="aura aura--buff aura--inspectable player-status-panel__synthetic"
|
||||
tabIndex={0}
|
||||
aria-label={`Absorb Shield, buff, ${shieldInspection.remainingLabel}`}
|
||||
aria-describedby={TOOLTIP_ID}
|
||||
onPointerEnter={() => setInspected(shieldInspection)}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.currentTarget !== document.activeElement) setInspected(null);
|
||||
}}
|
||||
onFocus={() => setInspected(shieldInspection)}
|
||||
onBlur={() => setInspected(null)}
|
||||
>
|
||||
<span className="aura__initial" aria-hidden="true">S</span>
|
||||
<b className="aura__timer" aria-hidden="true">
|
||||
{shieldExpiresAt > now ? formatTimedEffectTimer(shieldExpiresAt - now) : ""}
|
||||
</b>
|
||||
</span>
|
||||
) : null}
|
||||
<AuraStrip
|
||||
targetId={PLAYER_AGGRO_ID}
|
||||
dispositions={["buff"]}
|
||||
className="player-status-panel__strip"
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
{partyAuraTargetIds.length ? (
|
||||
<AuraStrip
|
||||
targetId={partyAuraTargetIds[0]}
|
||||
targetIds={partyAuraTargetIds}
|
||||
dispositions={["buff"]}
|
||||
className="player-status-panel__strip"
|
||||
showOnlyLongTerm
|
||||
targetLabels={partyAuraTargetLabels}
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
) : null}
|
||||
<TimedEffectStrip
|
||||
targetId={null}
|
||||
targetIds={PLAYER_EFFECT_TARGETS}
|
||||
kinds={["hot"]}
|
||||
className="player-status-panel__strip"
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
{!buffCount ? <span className="player-status-panel__empty">None active</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="player-status-panel__section player-status-panel__section--debuffs"
|
||||
aria-label={`${debuffCount} active ${debuffCount === 1 ? "debuff" : "debuffs"}`}
|
||||
>
|
||||
<header><strong>Debuffs</strong><b>{debuffCount}</b></header>
|
||||
<div className="player-status-panel__icons">
|
||||
<AuraStrip
|
||||
targetId={PLAYER_AGGRO_ID}
|
||||
dispositions={["debuff"]}
|
||||
className="player-status-panel__strip"
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
{partyAuraTargetIds.length ? (
|
||||
<AuraStrip
|
||||
targetId={partyAuraTargetIds[0]}
|
||||
targetIds={partyAuraTargetIds}
|
||||
dispositions={["debuff"]}
|
||||
className="player-status-panel__strip"
|
||||
showOnlyLongTerm
|
||||
targetLabels={partyAuraTargetLabels}
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
) : null}
|
||||
<TimedEffectStrip
|
||||
targetId={null}
|
||||
targetIds={PLAYER_EFFECT_TARGETS}
|
||||
kinds={["dot"]}
|
||||
className="player-status-panel__strip"
|
||||
onInspect={setInspected}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
{!debuffCount ? <span className="player-status-panel__empty">None active</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{liveInspection ? (
|
||||
<div
|
||||
id={TOOLTIP_ID}
|
||||
className={`player-status-tooltip player-status-tooltip--${liveInspection.disposition}`}
|
||||
role="tooltip"
|
||||
>
|
||||
<header>
|
||||
<strong>{liveInspection.name}</strong>
|
||||
<span>{statusLabel(liveInspection.disposition)}</span>
|
||||
</header>
|
||||
<p>{liveInspection.description}</p>
|
||||
<footer>
|
||||
<span>{liveInspection.remainingLabel}</span>
|
||||
{liveInspection.stacks > 1 ? <span>{liveInspection.stacks} stacks</span> : null}
|
||||
{liveInspection.dispelCategory ? <span>{liveInspection.dispelCategory}</span> : null}
|
||||
{liveInspection.targetLabel ? <span>Target: {liveInspection.targetLabel}</span> : null}
|
||||
</footer>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user