/**
* @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.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 (
);
}
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 };