forked from phenom/healer-man
Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
||||
import { classById, type ClassId } from "../app/characterCatalog";
|
||||
import { classById, type ClassId, type GenderId, type RaceId } from "../app/characterCatalog";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import {
|
||||
EQUIPMENT_SLOTS,
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
equipmentSlotForItem,
|
||||
type EquipmentSlot,
|
||||
} from "../game/equipment";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import {
|
||||
auraAdjustedCharacterStats,
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
useCombatStore,
|
||||
} from "../game/combatStore";
|
||||
import {
|
||||
itemStatSummary,
|
||||
weaponDps,
|
||||
@@ -29,6 +33,7 @@ import {
|
||||
} from "../game/heirloomEquipment";
|
||||
import { gearProfileFor } from "../game/specializationCatalog";
|
||||
import type { PartyRole } from "../game/partyRoles";
|
||||
import type { CharacterStatRuleset } from "../game/characterStatRuleset";
|
||||
|
||||
const SLOT_LABELS: Readonly<Record<EquipmentSlot, string>> = Object.freeze({
|
||||
head: "Head",
|
||||
@@ -54,6 +59,9 @@ interface EquipmentOwner {
|
||||
readonly detail: string;
|
||||
readonly classId: ClassId;
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly raceId: RaceId;
|
||||
readonly gender: GenderId;
|
||||
readonly statRuleset: CharacterStatRuleset;
|
||||
readonly specializationId: string | null;
|
||||
readonly role: PartyRole;
|
||||
readonly level: number;
|
||||
@@ -129,6 +137,41 @@ function itemStatRows(item: InventoryItem): readonly { label: string; value: str
|
||||
}
|
||||
|
||||
function characterRows(stats: CharacterStats, page: StatPage): readonly { label: string; value: string }[] {
|
||||
const native = stats.romNative;
|
||||
if (native) {
|
||||
if (page === "attributes") return [
|
||||
{ label: "Strength (STR)", value: displayNumber(native.attributes.strength) },
|
||||
{ label: "Stamina (STA)", value: displayNumber(native.attributes.stamina) },
|
||||
{ label: "Intellect (INT)", value: displayNumber(native.attributes.intellect) },
|
||||
{ label: "Wisdom (WIS)", value: displayNumber(native.attributes.wisdom) },
|
||||
{ label: "Agility (AGI)", value: displayNumber(native.attributes.agility) },
|
||||
{ label: "Health", value: displayNumber(native.health.max) },
|
||||
...(native.usesMana ? [{ label: "Mana (MP)", value: displayNumber(native.mana.max) }] : []),
|
||||
{ label: "Health recovery", value: displayNumber(native.healthRecovery, 1) },
|
||||
...(native.usesMana ? [{ label: "Mana recovery", value: displayNumber(native.manaRecovery, 1) }] : []),
|
||||
];
|
||||
if (page === "offense") return [
|
||||
{ label: "Physical attack (ATK)", value: displayNumber(native.physicalAttack) },
|
||||
{ label: "Physical damage (DMG)", value: displayNumber(native.physicalDamage) },
|
||||
{ label: "Magical attack (MATK)", value: displayNumber(native.magicalAttack) },
|
||||
{ label: "Magical damage (MDMG)", value: displayNumber(native.magicalDamage) },
|
||||
{ label: "Magical damage points", value: displayNumber(native.magicalDamagePoint) },
|
||||
{ label: "Magic power rate", value: `×${native.magicPowerRate.toFixed(3)}` },
|
||||
{ label: "Physical hit (points)", value: displayNumber(native.physicalHit, 2) },
|
||||
{ label: "Magical hit (points)", value: displayNumber(native.magicalHit, 2) },
|
||||
{ label: "Physical critical (points)", value: displayNumber(native.physicalCritical, 2) },
|
||||
{ label: "Magical critical (points)", value: displayNumber(native.magicalCritical, 2) },
|
||||
{ label: "Healing power rate", value: `×${native.healing.healPowerRate.toFixed(3)}` },
|
||||
{ label: "Healing points", value: displayNumber(native.healing.healPoint) },
|
||||
];
|
||||
return [
|
||||
{ label: "Physical defense (DEF)", value: `${displayNumber(native.physicalDefense)} · ${stats.armorReductionPercent.toFixed(2)}% shared armor rule` },
|
||||
{ label: "Magical defense (MDEF)", value: displayNumber(native.magicalDefense) },
|
||||
{ label: "Physical dodge (points)", value: displayNumber(native.physicalDodge, 2) },
|
||||
{ label: "Magical dodge (points)", value: displayNumber(native.magicalDodge, 2) },
|
||||
{ label: "Parry (points)", value: displayNumber(native.parry, 2) },
|
||||
];
|
||||
}
|
||||
if (page === "attributes") {
|
||||
return [
|
||||
{ label: "Strength", value: displayNumber(stats.strength) },
|
||||
@@ -190,10 +233,14 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
const inventory = useCombatStore((state) => state.inventory);
|
||||
const equipment = useCombatStore((state) => state.equipment);
|
||||
const playerClassId = useCombatStore((state) => state.classId);
|
||||
const playerStatRuleset = useCombatStore((state) => state.statRuleset);
|
||||
const playerRaceId = useCombatStore((state) => state.raceId);
|
||||
const playerGender = useCombatStore((state) => state.gender);
|
||||
const playerSecondaryClassId = useCombatStore((state) => state.secondaryClassId);
|
||||
const playerLevel = useCombatStore((state) => state.level);
|
||||
const playerMaxHealth = useCombatStore((state) => state.maxHealth);
|
||||
const playerGearStats = useCombatStore((state) => state.gearStats);
|
||||
const activeAuras = useCombatStore((state) => state.auras);
|
||||
const playerSpecializationId = useCombatStore((state) => state.specializationId);
|
||||
const playerGearRole = useCombatStore((state) => state.gearRole);
|
||||
const feedback = useCombatStore((state) => state.feedback);
|
||||
@@ -207,6 +254,9 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
detail: "Player",
|
||||
classId: playerClassId,
|
||||
secondaryClassId: playerSecondaryClassId,
|
||||
raceId: playerRaceId,
|
||||
gender: playerGender,
|
||||
statRuleset: playerStatRuleset,
|
||||
specializationId: playerSpecializationId,
|
||||
role: playerGearRole,
|
||||
level: playerLevel,
|
||||
@@ -218,12 +268,15 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
detail: `${member.specialization} · ${member.role === "damage" ? "DPS" : member.role}`,
|
||||
classId: member.classId,
|
||||
secondaryClassId: member.secondaryClassId,
|
||||
raceId: member.raceId,
|
||||
gender: member.gender,
|
||||
statRuleset: member.statRuleset ?? "wow",
|
||||
specializationId: member.specializationId,
|
||||
role: member.role,
|
||||
level: member.level,
|
||||
ownerKind: "party" as const,
|
||||
})),
|
||||
], [character?.name, members, playerClassId, playerSecondaryClassId, playerGearRole, playerLevel, playerSpecializationId]);
|
||||
], [character?.name, members, playerClassId, playerSecondaryClassId, playerRaceId, playerGender, playerStatRuleset, playerGearRole, playerLevel, playerSpecializationId]);
|
||||
const [ownerId, setOwnerId] = useState(PLAYER_EQUIPMENT_OWNER_ID);
|
||||
const [inspectedId, setInspectedId] = useState(() => inventory.at(-1)?.instanceId ?? "");
|
||||
const [statPage, setStatPage] = useState<StatPage>("attributes");
|
||||
@@ -300,8 +353,23 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
const ownerLevel = selectedMember?.level ?? playerLevel;
|
||||
const ownerMaxHealth = selectedMember?.maxHealth ?? playerMaxHealth;
|
||||
const ownerGearStats = selectedMember?.gearStats ?? playerGearStats;
|
||||
const ownerRaceId = selectedMember?.raceId ?? character?.raceId ?? "human";
|
||||
const ownerStats = deriveCharacterStats(owner.classId, ownerLevel, ownerGearStats, ownerRaceId);
|
||||
const ownerBaseStats = deriveCharacterStats(
|
||||
owner.classId,
|
||||
ownerLevel,
|
||||
ownerGearStats,
|
||||
owner.raceId,
|
||||
{},
|
||||
owner.secondaryClassId,
|
||||
owner.gender,
|
||||
owner.statRuleset,
|
||||
);
|
||||
const ownerStats = ownerBaseStats.romNative
|
||||
? auraAdjustedCharacterStats(
|
||||
ownerBaseStats,
|
||||
activeAuras,
|
||||
owner.ownerKind === "player" ? PLAYER_AURA_ENTITY_ID : owner.id,
|
||||
)
|
||||
: ownerBaseStats;
|
||||
const visibleCharacterRows = characterRows(ownerStats, statPage);
|
||||
const inspectedStatRows = inspected ? itemStatRows(inspected) : [];
|
||||
|
||||
@@ -378,6 +446,9 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
>{page}</ControllerButton>
|
||||
))}
|
||||
</div>
|
||||
{owner.statRuleset === "rom" && (
|
||||
<p className="equipment-character-stats__note">Native RoM stats · shared dungeon combat rules</p>
|
||||
)}
|
||||
<dl className="equipment-character-stats" aria-label={`${owner.name} ${statPage} stats`}>
|
||||
{visibleCharacterRows.map((row) => (
|
||||
<div key={row.label}><dt>{row.label}</dt><dd>{row.value}</dd></div>
|
||||
@@ -385,7 +456,9 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
|
||||
</dl>
|
||||
<div className="equipment-character-vitals">
|
||||
<span>Combat health <strong>{displayNumber(ownerMaxHealth)}</strong></span>
|
||||
<span>Gear stamina <strong>+{displayNumber(ownerGearStats.stamina)}</strong></span>
|
||||
<span>Gear stamina <strong>+{displayNumber(
|
||||
ownerStats.romNative?.sourceInputs.gear.stamina ?? ownerGearStats.stamina,
|
||||
)}</strong></span>
|
||||
</div>
|
||||
<div className="equipment-loadout__slots">
|
||||
{EQUIPMENT_SLOTS.map((slot) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
||||
import { useCompanionPartyPreferences } from "../game/companionPartyPreferences";
|
||||
import { partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyStore";
|
||||
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
|
||||
import { normalizeCharacterStatRuleset } from "../game/characterStatRuleset";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
@@ -163,6 +164,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
const openManastorms = useShellStore((state) => state.openManastorms);
|
||||
const openArenas = useShellStore((state) => state.openArenas);
|
||||
const openClassHall = useShellStore((state) => state.openClassHall);
|
||||
const setSelectedCharacterStatRuleset = useShellStore((state) => state.setSelectedCharacterStatRuleset);
|
||||
const returnToCharacters = useShellStore((state) => state.returnToCharacters);
|
||||
const notice = useShellStore((state) => state.notice);
|
||||
const session = useShellStore((state) => state.session);
|
||||
@@ -260,6 +262,10 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
{ id: "mode-keybinds", run: () => onOpenEditor("keybinds") },
|
||||
{ id: "mode-bindings", run: () => onOpenEditor("bindings") },
|
||||
] : []),
|
||||
...(character && isRom ? [
|
||||
{ id: "stat-profile-rom", run: () => setSelectedCharacterStatRuleset("rom"), enabled: !onlineGroup?.activity },
|
||||
{ id: "stat-profile-wow", run: () => setSelectedCharacterStatRuleset("wow"), enabled: !onlineGroup?.activity },
|
||||
] : []),
|
||||
{ id: "mode-cards", run: onOpenCards },
|
||||
{ id: "mode-gm-menu", run: () => setGmMenuOpen(true) },
|
||||
...(SHELVED_MODES_ENABLED ? [
|
||||
@@ -268,7 +274,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
] : []),
|
||||
{ id: "mode-downloads", run: onOpenDownloads },
|
||||
{ id: "back-to-roster", run: returnToCharacters, neighbors: { down: "mode-dungeons" } },
|
||||
], [availableRoles, character, closeRandomQueue, gmMenuOpen, isGm, onOpenBalanceLab, onOpenBalanceTuning, onOpenPartyBuffPolicy, onOpenDungeonQueuePolicy, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenDungeonRace, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, openRandomBiteSizedDungeon, openRandomDungeon, queueing, randomQueueMode, returnToCharacters, selectedRole, startRandomQueue]);
|
||||
], [availableRoles, character, closeRandomQueue, gmMenuOpen, isGm, isRom, onOpenBalanceLab, onOpenBalanceTuning, onOpenPartyBuffPolicy, onOpenDungeonQueuePolicy, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenDungeonRace, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, onlineGroup?.activity, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, openRandomBiteSizedDungeon, openRandomDungeon, queueing, randomQueueMode, returnToCharacters, selectedRole, setSelectedCharacterStatRuleset, startRandomQueue]);
|
||||
const controller = useMenuController(actions, {
|
||||
columns: 2,
|
||||
initialId: randomQueueMode ? `random-queue-role-${availableRoles[0]}` : gmMenuOpen ? (isGm ? "gm-menu-gm-tools" : "gm-menu-boss-testing") : "mode-dungeons",
|
||||
@@ -276,6 +282,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
});
|
||||
|
||||
const characterClass = character ? classById(character.classId) : null;
|
||||
const statRuleset = character ? normalizeCharacterStatRuleset(character.statRuleset, character.classId) : "wow";
|
||||
const specialization = specializationById(character?.specializationId);
|
||||
const secondaryClass = character?.secondaryClassId ? classById(character.secondaryClassId) : null;
|
||||
const gameModeAction = (id: string) => {
|
||||
@@ -348,6 +355,33 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
|
||||
? secondaryClass ? ` / ${secondaryClass.name}` : " / No secondary class"
|
||||
: specialization ? ` / ${specialization.displayName}` : ""}
|
||||
</strong>
|
||||
{isRom && <fieldset style={{ display: "grid", gap: 5, margin: "8px 0 0", padding: "6px 8px", border: "1px solid rgba(159,217,223,.24)" }}>
|
||||
<legend style={{ padding: "0 4px", color: "#9fb2b0", fontSize: 8, letterSpacing: ".08em", textTransform: "uppercase" }}>Stat profile</legend>
|
||||
<div role="group" aria-label="RoM stat profile" style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
|
||||
{([
|
||||
["rom", "Native RoM", "stat-profile-rom"],
|
||||
["wow", "WoW compatibility", "stat-profile-wow"],
|
||||
] as const).map(([ruleset, label, controlId]) => (
|
||||
<ControllerButton
|
||||
key={ruleset}
|
||||
controlId={controlId}
|
||||
selectedId={controller.selectedId}
|
||||
select={controller.select}
|
||||
className="front-button"
|
||||
type="button"
|
||||
aria-pressed={statRuleset === ruleset}
|
||||
disabled={Boolean(onlineGroup?.activity)}
|
||||
onClick={() => setSelectedCharacterStatRuleset(ruleset)}
|
||||
style={{
|
||||
flex: "1 1 110px", minWidth: 0, padding: "6px 8px", fontSize: 8,
|
||||
background: statRuleset === ruleset ? "rgba(64,116,121,.5)" : undefined,
|
||||
borderColor: statRuleset === ruleset ? "#9fd9df" : undefined,
|
||||
}}
|
||||
>{label}</ControllerButton>
|
||||
))}
|
||||
</div>
|
||||
{onlineGroup?.activity && <small style={{ color: "#9fb2b0", fontSize: 8 }}>Unavailable during a live online group activity.</small>}
|
||||
</fieldset>}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -232,9 +232,9 @@ function MemberFrame({ member, selected, onSelect, healPopups }: {
|
||||
<span className="party-frame__health" role="progressbar" aria-label={`${member.name} health`} aria-valuemin={0} aria-valuemax={member.maxHealth} aria-valuenow={Math.max(0, member.health)}>
|
||||
<i style={{ width: `${percent}%` }} />
|
||||
{absorb > 0 ? <em style={{ left: `${percent}%`, width: `${absorbPercent}%` }} /> : null}
|
||||
<b>{Math.ceil(member.health)} / {member.maxHealth}</b>
|
||||
<b>{Math.ceil(member.health)} / {Math.ceil(member.maxHealth)}</b>
|
||||
</span>
|
||||
<span className={`party-frame__resource party-frame__resource--${member.resourceType}`} title={`${Math.floor(member.resource)} / ${member.maxResource} ${member.resourceName}`}>
|
||||
<span className={`party-frame__resource party-frame__resource--${member.resourceType}`} title={`${Math.floor(member.resource)} / ${Math.floor(member.maxResource)} ${member.resourceName}`}>
|
||||
<i style={{ width: `${resourcePercent}%` }} />
|
||||
</span>
|
||||
<TimedEffectStrip targetId={member.id} className="timed-effect-strip--party" />
|
||||
@@ -283,9 +283,9 @@ function OnlineMemberFrame({
|
||||
<>
|
||||
<span className="party-frame__health" role="progressbar" aria-label={`${character.name} health`} aria-valuemin={0} aria-valuemax={player.maxHealth} aria-valuenow={Math.max(0, player.health)}>
|
||||
<i style={{ width: `${percent}%` }} />
|
||||
<b>{Math.ceil(player.health)} / {player.maxHealth}</b>
|
||||
<b>{Math.ceil(player.health)} / {Math.ceil(player.maxHealth)}</b>
|
||||
</span>
|
||||
<span className="party-frame__resource" title={`${Math.floor(player.resource)} / ${player.maxResource} ${player.resourceName}`}>
|
||||
<span className="party-frame__resource" title={`${Math.floor(player.resource)} / ${Math.floor(player.maxResource)} ${player.resourceName}`}>
|
||||
<i style={{ width: `${healthPercent(player.resource, player.maxResource)}%` }} />
|
||||
</span>
|
||||
<span className="party-frame__online-status"><i /> Live</span>
|
||||
@@ -383,9 +383,9 @@ export function PartyFrames() {
|
||||
<span className="party-frame__health" role="progressbar" aria-label="Player health" aria-valuemin={0} aria-valuemax={playerMaxHealth} aria-valuenow={Math.max(0, playerHealth)}>
|
||||
<i style={{ width: `${playerPercent}%` }} />
|
||||
{playerAbsorb > 0 ? <em style={{ left: `${playerPercent}%`, width: `${playerAbsorbPercent}%` }} /> : null}
|
||||
<b>{Math.ceil(playerHealth)} / {playerMaxHealth}</b>
|
||||
<b>{Math.ceil(playerHealth)} / {Math.ceil(playerMaxHealth)}</b>
|
||||
</span>
|
||||
<span className="party-frame__resource" title={`${Math.floor(playerResource)} / ${playerMaxResource} ${playerResourceName}`}>
|
||||
<span className="party-frame__resource" title={`${Math.floor(playerResource)} / ${Math.floor(playerMaxResource)} ${playerResourceName}`}>
|
||||
<i style={{ width: `${healthPercent(playerResource, playerMaxResource)}%` }} />
|
||||
</span>
|
||||
<TimedEffectStrip
|
||||
|
||||
+31
-1
@@ -1,5 +1,10 @@
|
||||
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);
|
||||
@@ -8,10 +13,21 @@ export function SummonFrames({ now }: { readonly now: number }) {
|
||||
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" aria-label="Pets, totems, and summons">
|
||||
<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
|
||||
@@ -48,6 +64,20 @@ export function SummonFrames({ now }: { readonly now: number }) {
|
||||
>{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
|
||||
|
||||
Reference in new issue
Block a user