forked from phenom/healer-man
109 lines
5.6 KiB
TypeScript
109 lines
5.6 KiB
TypeScript
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
|
import { useCombatStore } from "../game/combatStore";
|
|
import { useShellStore } from "../app/shellStore";
|
|
import {
|
|
isWow335HunterPetFamily,
|
|
WOW335_HUNTER_PET_FAMILY_CHOICES,
|
|
} from "../game/wow335HunterPetFamilySelection";
|
|
|
|
export function SummonFrames({ now }: { readonly now: number }) {
|
|
const summons = useCombatStore((state) => state.summons);
|
|
const selectedFriendlyActorId = useCombatStore((state) => state.selectedFriendlyActorId);
|
|
const selectedTargetId = useCombatStore((state) => state.selectedTargetId);
|
|
const selectFriendlyActor = useCombatStore((state) => state.selectFriendlyActor);
|
|
const commandHunterPet = useCombatStore((state) => state.commandHunterPet);
|
|
const setHunterPetReactMode = useCombatStore((state) => state.setHunterPetReactMode);
|
|
const hunterPetFamily = useCombatStore((state) => state.hunterPetFamily);
|
|
const setHunterPetFamily = useCombatStore((state) => state.setHunterPetFamily);
|
|
const mobs = useCombatStore((state) => state.mobs);
|
|
const setHunterPetGrowlAutocast = useCombatStore((state) => state.setHunterPetGrowlAutocast);
|
|
const hunterPetFamilyLocked = Object.values(mobs).some((mob) => mob.engaged && !mob.dead && !mob.despawned);
|
|
const hasPlayerHunterPet = summons.some((summon) => summon.hunterPet && summon.ownerActorId === PLAYER_AGGRO_ID);
|
|
const chooseHunterPetFamily = (value: unknown) => {
|
|
if (!isWow335HunterPetFamily(value)) return;
|
|
const previousFamily = hunterPetFamily;
|
|
if (!setHunterPetFamily(value)) return;
|
|
if (!useShellStore.getState().saveActiveHunterPetFamily(value)) setHunterPetFamily(previousFamily);
|
|
};
|
|
if (!summons.length) return null;
|
|
return (
|
|
<section className={`summon-frames${hasPlayerHunterPet ? " summon-frames--player-hunter-pet" : ""}`} aria-label="Pets, totems, and summons">
|
|
{summons.map((summon) => {
|
|
const healthPercent = summon.maxHealth > 0 ? Math.max(0, Math.min(100, summon.health / summon.maxHealth * 100)) : 0;
|
|
const remaining = summon.expiresAt >= Number.MAX_SAFE_INTEGER
|
|
? "∞"
|
|
: `${Math.max(0, Math.ceil((summon.expiresAt - now) / 1_000))}s`;
|
|
const playerHunterPet = Boolean(summon.hunterPet && summon.ownerActorId === PLAYER_AGGRO_ID);
|
|
const command = summon.hunterPetCommand ?? "follow";
|
|
const reactMode = summon.hunterPetReactMode ?? "defensive";
|
|
return (
|
|
<div
|
|
className={`summon-frame summon-frame--${summon.kind}${selectedFriendlyActorId === summon.id ? " is-selected" : ""}`}
|
|
key={summon.id}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="summon-frame__select"
|
|
aria-pressed={selectedFriendlyActorId === summon.id}
|
|
onClick={() => selectFriendlyActor(selectedFriendlyActorId === summon.id ? null : summon.id)}
|
|
>
|
|
<header><strong>{summon.name}</strong><span>{summon.kind} · {remaining}</span></header>
|
|
<div><i style={{ width: `${healthPercent}%` }} /></div>
|
|
<small>{Math.ceil(summon.health)} / {summon.maxHealth}</small>
|
|
</button>
|
|
{playerHunterPet && (
|
|
<div className="hunter-pet-controls" aria-label={`${summon.name} controls`}>
|
|
<div className="hunter-pet-controls__commands" role="group" aria-label="Pet command">
|
|
{(["attack", "follow", "stay"] as const).map((petCommand) => (
|
|
<button
|
|
key={petCommand}
|
|
type="button"
|
|
aria-pressed={command === petCommand}
|
|
disabled={petCommand === "attack" && !selectedTargetId}
|
|
onClick={() => commandHunterPet(summon.id, petCommand)}
|
|
>{petCommand}</button>
|
|
))}
|
|
</div>
|
|
<label className="hunter-pet-controls__react">
|
|
<span>Family</span>
|
|
<select
|
|
aria-label="Pet family"
|
|
value={hunterPetFamily}
|
|
disabled={hunterPetFamilyLocked}
|
|
title={hunterPetFamilyLocked ? "End the current encounter before changing pet family." : undefined}
|
|
onChange={(event) => chooseHunterPetFamily(event.currentTarget.value)}
|
|
>
|
|
{WOW335_HUNTER_PET_FAMILY_CHOICES.map((family) => (
|
|
<option key={family.id} value={family.id}>{family.name}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="hunter-pet-controls__react">
|
|
<span>React</span>
|
|
<select
|
|
aria-label="Pet reaction"
|
|
value={reactMode}
|
|
onChange={(event) => setHunterPetReactMode(summon.id, event.currentTarget.value as "defensive" | "aggressive" | "passive")}
|
|
>
|
|
<option value="defensive">Defensive</option>
|
|
<option value="aggressive">Aggressive</option>
|
|
<option value="passive">Passive</option>
|
|
</select>
|
|
</label>
|
|
<label className="hunter-pet-controls__growl">
|
|
<input
|
|
type="checkbox"
|
|
checked={Boolean(summon.hunterPetGrowlAutocastEnabled)}
|
|
onChange={(event) => setHunterPetGrowlAutocast(summon.id, event.currentTarget.checked)}
|
|
/>
|
|
Growl auto
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
);
|
|
}
|