/** * @file Boss panel component for viewing and challenging zone bosses. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable complexity -- Boss card requires many conditional render paths */ /* eslint-disable max-statements -- Boss panel requires many variable declarations */ /* eslint-disable max-lines -- Boss panel with sub-component and helper function */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { cdnImage } from "../../utils/cdn.js"; import { LockToggle } from "../ui/lockToggle.js"; import { ZoneSelector } from "./zoneSelector.js"; import type { Boss, GameState } from "@elysium/types"; interface BossCardProperties { readonly boss: Boss; readonly prestigeCount: number; readonly onChallenge: (bossId: string)=> void; readonly isChallenging: boolean; readonly unlockHint: string | undefined; readonly formatNumber: (n: number)=> string; } /** * Renders a single boss card. * @param props - The boss card properties. * @param props.boss - The boss data. * @param props.prestigeCount - The current prestige count for lock checking. * @param props.onChallenge - Callback to challenge this boss. * @param props.isChallenging - Whether this boss is currently being challenged. * @param props.unlockHint - Optional hint for how to unlock this boss. * @param props.formatNumber - The number formatting utility function. * @returns The JSX element. */ const BossCard = ({ boss, prestigeCount, onChallenge, isChallenging, unlockHint, formatNumber, }: BossCardProperties): JSX.Element => { const scaled = boss.currentHp * 100; const hpPercent = scaled / boss.maxHp; const isPrestigeLocked = boss.prestigeRequirement > prestigeCount; const canChallenge = (boss.status === "available" || boss.status === "in_progress") && !isChallenging; function handleChallenge(): void { onChallenge(boss.id); } return (
{boss.name}

{boss.name}

{boss.description}

{isPrestigeLocked && boss.status === "locked" ?

{"๐Ÿ”’ Requires Prestige "} {boss.prestigeRequirement}

: null} {!isPrestigeLocked && boss.status === "locked" && unlockHint !== undefined ?

{unlockHint}

: null}
{boss.status !== "locked" && boss.status !== "defeated" &&
{formatNumber(boss.currentHp)} {" / "} {formatNumber(boss.maxHp)} {" HP"}
}
{"๐Ÿ’ข Boss DPS: "} {formatNumber(boss.damagePerSecond)}
{"๐Ÿช™ "} {formatNumber(boss.goldReward)} {boss.essenceReward > 0 && {"โœจ "} {formatNumber(boss.essenceReward)} } {boss.crystalReward > 0 && {"๐Ÿ’Ž "} {formatNumber(boss.crystalReward)} } {boss.equipmentRewards.length > 0 && {"๐Ÿ—ก๏ธ "} {boss.equipmentRewards.length} {" Equipment"} } {boss.status !== "defeated" && boss.bountyRunestones > 0 && boss.bountyRunestonesClaimed !== true && {"๐Ÿ”ฎ "} {boss.bountyRunestones} {" (first kill)"} }
{(boss.status === "available" || boss.status === "in_progress") && } {boss.status === "defeated" && {"โ˜ ๏ธ Defeated"} }
); }; /** * Computes party DPS and HP from the current game state. * @param state - The full game state. * @returns The computed party DPS and HP values. */ const computePartyStats = ( state: GameState, ): { partyDps: number; partyHp: number; } => { const { upgrades, adventurers, equipment, prestige } = state; let globalMultiplier = 1; for (const upgrade of upgrades) { const { purchased, target, multiplier } = upgrade; if (purchased && target === "global") { globalMultiplier = globalMultiplier * multiplier; } } const prestigeBonus = prestige.count * 0.1; const prestigeMultiplier = 1 + prestigeBonus; const equipmentCombatMultiplier = equipment. filter((item) => { return item.equipped && item.bonus.combatMultiplier !== undefined; }). reduce((multiplier, item) => { return multiplier * (item.bonus.combatMultiplier ?? 1); }, 1); let partyDps = 0; let partyHp = 0; for (const adventurer of adventurers) { const { count, id: adventurerId, combatPower, level } = adventurer; if (count === 0) { continue; } let adventurerMultiplier = 1; for (const upgrade of upgrades) { const { purchased, target, multiplier, adventurerId: upgradeAdventurerId, } = upgrade; if ( purchased && target === "adventurer" && upgradeAdventurerId === adventurerId ) { adventurerMultiplier = adventurerMultiplier * multiplier; } } const dps = combatPower * count * adventurerMultiplier * globalMultiplier * prestigeMultiplier; partyDps = partyDps + dps; const hp = level * 50 * count; partyHp = partyHp + hp; } partyDps = partyDps * equipmentCombatMultiplier; return { partyDps, partyHp }; }; /** * Renders the boss panel with zone selection and boss list. * @returns The JSX element. */ const BossPanel = (): JSX.Element => { const { state, challengeBoss, formatNumber, toggleAutoBoss, autoBossLastResult, autoBossError, } = useGame(); const [ challengingBossId, setChallengingBossId ] = useState( null, ); const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale"); const [ showLocked, setShowLocked ] = useState(true); if (state === null) { return (

{"Loading..."}

); } async function handleChallenge(bossId: string): Promise { setChallengingBossId(bossId); try { await challengeBoss(bossId); } finally { setChallengingBossId(null); } } function handleChallengeClick(bossId: string): void { void handleChallenge(bossId); } const { zones, bosses, quests, autoBoss, prestige: playerPrestige } = state; const zoneBosses = bosses.filter((boss) => { return boss.zoneId === activeZoneId; }); const lockedCount = zoneBosses.filter((boss) => { return boss.status === "locked"; }).length; const visibleBosses = showLocked ? zoneBosses : zoneBosses.filter((boss) => { return boss.status !== "locked"; }); const bossUnlockHints = new Map(); for (const zone of zones) { const { id: zoneId, unlockBossId, unlockQuestId } = zone; const allZoneBosses = bosses.filter((boss) => { return boss.zoneId === zoneId; }); for (let index = 0; index < allZoneBosses.length; index = index + 1) { const boss = allZoneBosses[index]; if (boss === undefined || boss.status !== "locked") { continue; } if (index === 0) { const parts: Array = []; if (unlockBossId !== null) { const gateBoss = bosses.find((candidate) => { return candidate.id === unlockBossId; }); if (gateBoss !== undefined) { parts.push(`โš”๏ธ Defeat: ${gateBoss.name}`); } } if (unlockQuestId !== null) { const gateQuest = quests.find((candidate) => { return candidate.id === unlockQuestId; }); if (gateQuest !== undefined) { parts.push(`๐Ÿ“œ Complete: ${gateQuest.name}`); } } if (parts.length > 0) { bossUnlockHints.set(boss.id, parts.join(" & ")); } } else { const previousBoss = allZoneBosses[index - 1]; if (previousBoss !== undefined) { bossUnlockHints.set(boss.id, `โš”๏ธ Defeat: ${previousBoss.name} first`); } } } } function handleToggle(): void { setShowLocked((current) => { return !current; }); } const autoBossOn = autoBoss === true; const { partyDps, partyHp } = computePartyStats(state); const { count: prestigeCount } = playerPrestige; return (

{"Boss Encounters"}

{autoBossError === null ? null :

{"โš ๏ธ Auto-boss stopped: "} {autoBossError}

} {autoBossLastResult !== null && autoBossError === null ?

{"๐Ÿค– Last fight: "} {autoBossLastResult.bossName} {autoBossLastResult.won ? " โ€” โœ… Won" : " โ€” โŒ Lost"}

: null}
{"โš”๏ธ Party DPS"} {formatNumber(partyDps)}
{"โค๏ธ Party HP"} {formatNumber(partyHp)}
{visibleBosses.map((boss) => { const { id: bossId } = boss; return ( ); })} {visibleBosses.length === 0 &&

{"No bosses to show in this zone."}

}
); }; export { BossPanel };