generated from nhcarrigan/template
fix: delay boss notifications until reveal and animate hp bar colours
- Move bossVictory sound and notification from gameContext into BattleModal, fired at the 5.2s reveal timeout so the animation plays before the spoiler - Replace CSS width transition with a setInterval tick (50ms steps over 5s) so bossHpPercent and partyHpPercent update incrementally during the animation - Both bars now use a shared getHpColour helper: green >50%, yellow 25-50%, red <25%, causing colour to shift naturally as the bar visually drains
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
/* 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.
|
||||
@@ -23,6 +25,22 @@ const toHpPercent = (current: number, maximum: number): number => {
|
||||
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;
|
||||
@@ -40,12 +58,11 @@ const BattleModal = ({
|
||||
onDismiss,
|
||||
}: BattleModalProperties): JSX.Element => {
|
||||
const { result, bossName } = battle;
|
||||
const { formatNumber } = useGame();
|
||||
const { enableNotifications, enableSounds, formatNumber } = useGame();
|
||||
|
||||
const [ phase, setPhase ] = useState<"animating" | "result">("animating");
|
||||
|
||||
const bossStartPercent = toHpPercent(result.bossHpBefore, result.bossMaxHp);
|
||||
const partyStartPercent = 100;
|
||||
|
||||
const bossEndPercent = toHpPercent(
|
||||
result.bossHpAtBattleEnd,
|
||||
@@ -57,37 +74,70 @@ const BattleModal = ({
|
||||
);
|
||||
|
||||
const [ bossHpPercent, setBossHpPercent ] = useState(bossStartPercent);
|
||||
const [ partyHpPercent, setPartyHpPercent ] = useState(partyStartPercent);
|
||||
const [ partyHpPercent, setPartyHpPercent ] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const startAnimation = setTimeout(() => {
|
||||
setBossHpPercent(bossEndPercent);
|
||||
setPartyHpPercent(partyEndPercent);
|
||||
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<typeof setInterval> | 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 revealResult = setTimeout(() => {
|
||||
const revealTimeout = setTimeout(() => {
|
||||
setPhase("result");
|
||||
if (result.won) {
|
||||
if (enableSounds) {
|
||||
playSound("bossVictory");
|
||||
}
|
||||
if (enableNotifications) {
|
||||
sendNotification("⚔️ Boss Defeated!", `You defeated ${bossName}!`);
|
||||
}
|
||||
}
|
||||
}, 5200);
|
||||
|
||||
return (): void => {
|
||||
clearTimeout(startAnimation);
|
||||
clearTimeout(revealResult);
|
||||
clearTimeout(startTimeout);
|
||||
clearTimeout(revealTimeout);
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [ bossEndPercent, partyEndPercent ]);
|
||||
}, [
|
||||
bossEndPercent,
|
||||
bossName,
|
||||
bossStartPercent,
|
||||
enableNotifications,
|
||||
enableSounds,
|
||||
partyEndPercent,
|
||||
result.won,
|
||||
]);
|
||||
|
||||
let bossHpBarColour = "#c0392b";
|
||||
if (bossHpPercent > 50) {
|
||||
bossHpBarColour = "#e74c3c";
|
||||
} else if (bossHpPercent > 25) {
|
||||
bossHpBarColour = "#e67e22";
|
||||
}
|
||||
|
||||
let partyHpBarColour = "#e74c3c";
|
||||
if (partyHpPercent > 50) {
|
||||
partyHpBarColour = "#27ae60";
|
||||
} else if (partyHpPercent > 25) {
|
||||
partyHpBarColour = "#f39c12";
|
||||
}
|
||||
const bossHpBarColour = getHpColour(bossHpPercent);
|
||||
const partyHpBarColour = getHpColour(partyHpPercent);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
@@ -120,7 +170,6 @@ const BattleModal = ({
|
||||
className="hp-bar-fill"
|
||||
style={{
|
||||
backgroundColor: bossHpBarColour,
|
||||
transition: "width 5s ease-in-out",
|
||||
width: `${bossHpPercent.toFixed(1)}%`,
|
||||
}}
|
||||
/>
|
||||
@@ -141,7 +190,6 @@ const BattleModal = ({
|
||||
className="hp-bar-fill party-hp"
|
||||
style={{
|
||||
backgroundColor: partyHpBarColour,
|
||||
transition: "width 5s ease-in-out",
|
||||
width: `${partyHpPercent.toFixed(1)}%`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1175,17 +1175,6 @@ export const GameProvider = ({
|
||||
return applyBossResult(previous, bossId, result);
|
||||
});
|
||||
setBattleResult({ bossName, result });
|
||||
if (result.won) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("bossVictory");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification(
|
||||
"⚔️ Boss Defeated!",
|
||||
`You defeated ${bossName}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}).
|
||||
catch(() => {
|
||||
|
||||
@@ -1785,14 +1774,6 @@ export const GameProvider = ({
|
||||
return applyBossResult(previous, bossId, result);
|
||||
});
|
||||
setBattleResult({ bossName: boss.name, result: result });
|
||||
if (result.won) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("bossVictory");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("⚔️ Boss Defeated!", `You defeated ${boss.name}!`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — server errors shouldn't crash the UI
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user