forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6cc1b9217f
commit
6114a2295f
67 files changed
+6241
-347
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
auraDefinitionDisplaysInStrip,
|
||||
auraDefinitionIcon,
|
||||
auraDefinitionIsDispellableDebuff,
|
||||
auraDefinitionIsFilteredLongTerm,
|
||||
auraDefinitionIsLongTerm,
|
||||
auraDescription,
|
||||
groupAurasForDisplay,
|
||||
@@ -65,6 +66,15 @@ describe("aura strip visibility", () => {
|
||||
expect(auraDefinitionIsLongTerm(aura({ durationMs: LONG_TERM_AURA_DURATION_MS - 1 }))).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps only explicitly retained persistent utility auras in clutter-filtered strips", () => {
|
||||
const bloodArrow = aura({ disposition: "buff", durationMs: null, utilityTags: ["rom-self-health-drain"] });
|
||||
const otherPersistentBuff = aura({ disposition: "buff", durationMs: null, utilityTags: ["other-persistent"] });
|
||||
|
||||
expect(auraDefinitionIsFilteredLongTerm(bloodArrow)).toBe(true);
|
||||
expect(auraDefinitionIsFilteredLongTerm(bloodArrow, ["rom-self-health-drain"])).toBe(false);
|
||||
expect(auraDefinitionIsFilteredLongTerm(otherPersistentBuff, ["rom-self-health-drain"])).toBe(true);
|
||||
});
|
||||
|
||||
it("identifies only categorized debuffs as dispellable player effects", () => {
|
||||
expect(auraDefinitionIsDispellableDebuff(aura({ dispelCategory: "magic" }))).toBe(true);
|
||||
expect(auraDefinitionIsDispellableDebuff(aura())).toBe(false);
|
||||
|
||||
+12
-1
@@ -1,12 +1,14 @@
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { usePresentationClock } from "./usePresentationClock";
|
||||
import type { AuraDisposition } from "../game/combatAuras";
|
||||
import type { ActiveAura } from "../game/combatAuras";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { formatTimedEffectTimer } from "../game/timedEffectDisplay";
|
||||
import {
|
||||
auraDefinitionDisplaysInStrip,
|
||||
auraDefinitionIcon,
|
||||
auraDefinitionIsDispellableDebuff,
|
||||
auraDefinitionIsFilteredLongTerm,
|
||||
auraDefinitionIsLongTerm,
|
||||
auraDescription,
|
||||
auraDisplayName,
|
||||
@@ -20,10 +22,12 @@ interface AuraStripProps {
|
||||
readonly dispositions?: readonly AuraDisposition[];
|
||||
readonly className?: string;
|
||||
readonly hideLongTerm?: boolean;
|
||||
readonly keepLongTermUtilityTags?: readonly string[];
|
||||
readonly showOnlyLongTerm?: boolean;
|
||||
readonly highlightDispellableDebuffs?: boolean;
|
||||
readonly targetLabels?: Readonly<Record<string, string>>;
|
||||
readonly onInspect?: StatusEffectInspectHandler;
|
||||
readonly onAuraContextMenu?: (aura: ActiveAura) => boolean;
|
||||
readonly tooltipId?: string;
|
||||
}
|
||||
|
||||
@@ -33,10 +37,12 @@ export function AuraStrip({
|
||||
dispositions = ["buff", "debuff"],
|
||||
className = "",
|
||||
hideLongTerm = false,
|
||||
keepLongTermUtilityTags = [],
|
||||
showOnlyLongTerm = false,
|
||||
highlightDispellableDebuffs = false,
|
||||
targetLabels,
|
||||
onInspect,
|
||||
onAuraContextMenu,
|
||||
tooltipId,
|
||||
}: AuraStripProps) {
|
||||
const requestedTargetIds = targetIds ?? [targetId];
|
||||
@@ -44,7 +50,7 @@ export function AuraStrip({
|
||||
requestedTargetIds.includes(aura.targetId)
|
||||
&& dispositions.includes(aura.definition.disposition)
|
||||
&& auraDefinitionDisplaysInStrip(aura.definition)
|
||||
&& (!hideLongTerm || !auraDefinitionIsLongTerm(aura.definition))
|
||||
&& (!hideLongTerm || !auraDefinitionIsFilteredLongTerm(aura.definition, keepLongTermUtilityTags))
|
||||
&& (!showOnlyLongTerm || auraDefinitionIsLongTerm(aura.definition))
|
||||
))));
|
||||
const hasTimedAuras = matchingAuras.some((aura) => aura.expiresAt !== null);
|
||||
@@ -93,6 +99,11 @@ export function AuraStrip({
|
||||
} : undefined}
|
||||
onFocus={onInspect ? () => onInspect(inspection) : undefined}
|
||||
onBlur={onInspect ? () => onInspect(null) : undefined}
|
||||
onContextMenu={onAuraContextMenu ? (event) => {
|
||||
if (!onAuraContextMenu(aura)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} : undefined}
|
||||
>
|
||||
{icon
|
||||
? <img className="aura__icon" src={icon} alt="" draggable={false} />
|
||||
|
||||
@@ -22,6 +22,10 @@ import { ResurrectionDestination, useResurrectionCheckpoints } from "./Resurrect
|
||||
import { useGameStore } from "../game/store";
|
||||
import { useHudSurface } from "./HudSurface";
|
||||
import { FrameDamageText } from "./FloatingDamageText";
|
||||
import {
|
||||
deactivateRomBloodArrowFromAuraContextMenu,
|
||||
ROM_SCOUT_BLOOD_ARROW_AURA_TAG,
|
||||
} from "../game/romBloodArrow";
|
||||
import {
|
||||
PARTY_HEAL_POPUP_LIFETIME_MS,
|
||||
addedPartyHealPopups,
|
||||
@@ -217,6 +221,7 @@ function MemberFrame({ member, selected, onSelect, healPopups }: {
|
||||
.map((aura) => aura.definition.name)
|
||||
.join(", "));
|
||||
const resourcePercent = healthPercent(member.resource, member.maxResource);
|
||||
const deactivateRomScoutBloodArrow = useCombatStore((state) => state.deactivateRomScoutBloodArrow);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -242,7 +247,13 @@ function MemberFrame({ member, selected, onSelect, healPopups }: {
|
||||
targetId={member.id}
|
||||
className="aura-strip--party"
|
||||
hideLongTerm
|
||||
keepLongTermUtilityTags={[ROM_SCOUT_BLOOD_ARROW_AURA_TAG]}
|
||||
highlightDispellableDebuffs
|
||||
onAuraContextMenu={(aura) => deactivateRomBloodArrowFromAuraContextMenu(
|
||||
aura,
|
||||
member.id,
|
||||
deactivateRomScoutBloodArrow,
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<HealPopups popups={healPopups} targetId={member.id} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { abilityById } from "../game/abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { formatTimedEffectTimer, groupTimedEffectsForDisplay, timedEffectRemainingMs } from "../game/timedEffectDisplay";
|
||||
import { deactivateRomBloodArrowFromAuraContextMenu } from "../game/romBloodArrow";
|
||||
import { AuraStrip } from "./AuraStrip";
|
||||
import {
|
||||
auraDisplaysInPlayerStatusPanel,
|
||||
@@ -26,6 +27,7 @@ function statusLabel(disposition: StatusEffectInspection["disposition"]): string
|
||||
export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
const auras = useCombatStore((state) => state.auras);
|
||||
const effects = useCombatStore((state) => state.effects);
|
||||
const deactivateRomScoutBloodArrow = useCombatStore((state) => state.deactivateRomScoutBloodArrow);
|
||||
const shield = useCombatStore((state) => state.shield);
|
||||
const shieldExpiresAt = useCombatStore((state) => state.shieldExpiresAt);
|
||||
const [inspected, setInspected] = useState<StatusEffectInspection | null>(null);
|
||||
@@ -131,6 +133,12 @@ export function PlayerStatusPanel({ now }: { readonly now: number }) {
|
||||
dispositions={["buff"]}
|
||||
className="player-status-panel__strip"
|
||||
onInspect={setInspected}
|
||||
onAuraContextMenu={(aura) => deactivateRomBloodArrowFromAuraContextMenu(
|
||||
aura,
|
||||
PLAYER_AGGRO_ID,
|
||||
deactivateRomScoutBloodArrow,
|
||||
now,
|
||||
)}
|
||||
tooltipId={TOOLTIP_ID}
|
||||
/>
|
||||
<TimedEffectStrip
|
||||
|
||||
@@ -57,6 +57,14 @@ export function auraDefinitionIsLongTerm(definition: AuraDefinition): boolean {
|
||||
return definition.durationMs === null || definition.durationMs >= LONG_TERM_AURA_DURATION_MS;
|
||||
}
|
||||
|
||||
export function auraDefinitionIsFilteredLongTerm(
|
||||
definition: AuraDefinition,
|
||||
keepLongTermUtilityTags: readonly string[] = [],
|
||||
): boolean {
|
||||
return auraDefinitionIsLongTerm(definition)
|
||||
&& !definition.utilityTags?.some((tag) => keepLongTermUtilityTags.includes(tag));
|
||||
}
|
||||
|
||||
export function auraDefinitionIsDispellableDebuff(definition: AuraDefinition): boolean {
|
||||
return definition.disposition === "debuff" && definition.dispelCategory !== undefined;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { activateDungeonEncounterDefinitions, resetDungeonEncounters } from "../
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { usePartyStore } from "../game/partyStore";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { ROM_SCOUT_BLOOD_ARROW_AURA_TAG } from "../game/romBloodArrow";
|
||||
import { PartyFrames } from "./PartyFrames";
|
||||
|
||||
// Exercise the client HUD against live stores rather than their initial SSR snapshots.
|
||||
@@ -79,6 +80,36 @@ describe("party resurrection UI", () => {
|
||||
expect(render()).toContain(`value="boss:${boss.id}"`);
|
||||
});
|
||||
|
||||
it("keeps an owned NPC's cancelable permanent Blood Arrow visible while filtering other permanent buffs", () => {
|
||||
const ownerId = usePartyStore.getState().members[0]!.id;
|
||||
const aura = (id: string, name: string, utilityTags?: readonly string[]) => ({
|
||||
instanceId: id,
|
||||
definition: {
|
||||
id,
|
||||
name,
|
||||
disposition: "buff" as const,
|
||||
durationMs: null,
|
||||
...(utilityTags ? { utilityTags } : {}),
|
||||
},
|
||||
sourceId: ownerId,
|
||||
targetId: ownerId,
|
||||
appliedAt: 1_000,
|
||||
expiresAt: null,
|
||||
stacks: 1,
|
||||
absorbs: [],
|
||||
procStates: [],
|
||||
});
|
||||
useCombatStore.setState({ auras: [
|
||||
aura("blood-arrow-permanent", "Blood Arrow", [ROM_SCOUT_BLOOD_ARROW_AURA_TAG]),
|
||||
aura("other-permanent", "Other Persistent Buff"),
|
||||
] });
|
||||
|
||||
const markup = render();
|
||||
|
||||
expect(markup).toContain("Blood Arrow, buff");
|
||||
expect(markup).not.toContain("Other Persistent Buff, buff");
|
||||
});
|
||||
|
||||
it("does not offer free resurrection in Manastorm", () => {
|
||||
killMember();
|
||||
useGameStore.setState({ gameMode: "manastorm" });
|
||||
|
||||
Reference in new issue
Block a user