/** * @file Battle modal component displaying animated battle results. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex battle animation and result display */ /* eslint-disable complexity -- Battle result display requires many conditional paths */ import { type JSX, useEffect, useState } from "react"; import { type BattleResult, useGame } from "../../context/gameContext.js"; import { sendNotification } from "../../utils/notification.js"; import { playSound } from "../../utils/sound.js"; /** * Converts HP values to a percentage for display. * @param current - The current HP value. * @param maximum - The maximum HP value. * @returns The percentage as a number between 0 and 100. */ const toHpPercent = (current: number, maximum: number): number => { if (maximum === 0) { return 0; } const scaled = current * 100; return scaled / maximum; }; /** * Returns a colour hex string based on the HP percentage. * Green above 50%, yellow 25–50%, red below 25%. * @param percent - Current HP as a percentage (0–100). * @returns A hex colour string. */ const getHpColour = (percent: number): string => { if (percent > 50) { return "#27ae60"; } if (percent > 25) { return "#f39c12"; } return "#e74c3c"; }; interface BattleModalProperties { readonly battle: BattleResult; readonly onDismiss: ()=> void; } /** * Renders the battle modal with HP bars and animated battle results. * @param props - The battle modal properties. * @param props.battle - The battle result data to display. * @param props.onDismiss - Callback to dismiss the modal. * @returns The JSX element. */ const BattleModal = ({ battle, onDismiss, }: BattleModalProperties): JSX.Element => { const { result, bossName } = battle; const { enableNotifications, enableSounds, flushBossLoreToasts, formatNumber, } = useGame(); const [ phase, setPhase ] = useState<"animating" | "result">("animating"); const bossStartPercent = toHpPercent(result.bossHpBefore, result.bossMaxHp); const bossEndPercent = toHpPercent( result.bossHpAtBattleEnd, result.bossMaxHp, ); const partyEndPercent = toHpPercent( result.partyHpRemaining, result.partyMaxHp, ); const [ bossHpPercent, setBossHpPercent ] = useState(bossStartPercent); const [ partyHpPercent, setPartyHpPercent ] = useState(100); useEffect(() => { const animationDurationMs = 5000; const intervalMs = 50; const totalSteps = animationDurationMs / intervalMs; const bossHpRange = bossEndPercent - bossStartPercent; const bossDelta = bossHpRange / totalSteps; const partyHpRange = partyEndPercent - 100; const partyDelta = partyHpRange / totalSteps; let currentStep = 0; // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned inside timeout let intervalId: ReturnType | undefined; const tick = (): void => { currentStep = currentStep + 1; if (currentStep >= totalSteps) { setBossHpPercent(bossEndPercent); setPartyHpPercent(partyEndPercent); clearInterval(intervalId); } else { const bossStep = bossDelta * currentStep; setBossHpPercent(bossStartPercent + bossStep); const partyStep = partyDelta * currentStep; setPartyHpPercent(100 + partyStep); } }; const startTimeout = setTimeout(() => { intervalId = setInterval(tick, intervalMs); }, 200); const revealTimeout = setTimeout(() => { setPhase("result"); flushBossLoreToasts(); if (result.won) { if (enableSounds) { playSound("bossVictory"); } if (enableNotifications) { sendNotification("⚔️ Boss Defeated!", `You defeated ${bossName}!`); } } }, 5200); return (): void => { clearTimeout(startTimeout); clearTimeout(revealTimeout); clearInterval(intervalId); }; }, [ bossEndPercent, bossName, bossStartPercent, enableNotifications, enableSounds, flushBossLoreToasts, partyEndPercent, result.won, ]); const bossHpBarColour = getHpColour(bossHpPercent); const partyHpBarColour = getHpColour(partyHpPercent); return (

{"⚔️ Battle: "} {bossName}

{"Your Party DPS"} {formatNumber(result.partyDPS)}
{"vs"}
{"Boss DPS"} {formatNumber(result.bossDPS)}
{"👹 "} {bossName}
{formatNumber(result.bossHpAtBattleEnd)} {" / "} {formatNumber(result.bossMaxHp)}
{"⚔️ VS ⚔️"}
{"🛡️ Your Party"}
{formatNumber(result.partyHpRemaining)} {" / "} {formatNumber(result.partyMaxHp)}
{phase === "animating" &&

{"Battling…"}

} {phase === "result" &&
{result.won ? <>

{"🏆 Victory!"}

{result.rewards === undefined ? null :

{"Rewards:"}

{"🪙 "} {formatNumber(result.rewards.gold)} {" gold"} {result.rewards.essence > 0 && {"✨ "} {formatNumber(result.rewards.essence)} {" essence"} } {result.rewards.crystals > 0 && {"💎 "} {formatNumber(result.rewards.crystals)} {" crystals"} } {result.rewards.bountyRunestones > 0 && {"🔮 "} {formatNumber(result.rewards.bountyRunestones)} {" runestones (first kill!)"} }
} : <>

{"💀 Defeat"}

{"Your party was defeated. The boss has reset."}

{result.casualties !== undefined && result.casualties.length > 0 ?

{"Casualties:"}

{result.casualties.map((casualty) => { return ( {"☠️ "} {casualty.killed} {casualty.adventurerId} {" lost"} ); })}
: null} }
}
); }; export { BattleModal };