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 */
|
/* eslint-disable complexity -- Battle result display requires many conditional paths */
|
||||||
import { type JSX, useEffect, useState } from "react";
|
import { type JSX, useEffect, useState } from "react";
|
||||||
import { type BattleResult, useGame } from "../../context/gameContext.js";
|
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.
|
* Converts HP values to a percentage for display.
|
||||||
@@ -23,6 +25,22 @@ const toHpPercent = (current: number, maximum: number): number => {
|
|||||||
return scaled / maximum;
|
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 {
|
interface BattleModalProperties {
|
||||||
readonly battle: BattleResult;
|
readonly battle: BattleResult;
|
||||||
readonly onDismiss: ()=> void;
|
readonly onDismiss: ()=> void;
|
||||||
@@ -40,12 +58,11 @@ const BattleModal = ({
|
|||||||
onDismiss,
|
onDismiss,
|
||||||
}: BattleModalProperties): JSX.Element => {
|
}: BattleModalProperties): JSX.Element => {
|
||||||
const { result, bossName } = battle;
|
const { result, bossName } = battle;
|
||||||
const { formatNumber } = useGame();
|
const { enableNotifications, enableSounds, formatNumber } = useGame();
|
||||||
|
|
||||||
const [ phase, setPhase ] = useState<"animating" | "result">("animating");
|
const [ phase, setPhase ] = useState<"animating" | "result">("animating");
|
||||||
|
|
||||||
const bossStartPercent = toHpPercent(result.bossHpBefore, result.bossMaxHp);
|
const bossStartPercent = toHpPercent(result.bossHpBefore, result.bossMaxHp);
|
||||||
const partyStartPercent = 100;
|
|
||||||
|
|
||||||
const bossEndPercent = toHpPercent(
|
const bossEndPercent = toHpPercent(
|
||||||
result.bossHpAtBattleEnd,
|
result.bossHpAtBattleEnd,
|
||||||
@@ -57,37 +74,70 @@ const BattleModal = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [ bossHpPercent, setBossHpPercent ] = useState(bossStartPercent);
|
const [ bossHpPercent, setBossHpPercent ] = useState(bossStartPercent);
|
||||||
const [ partyHpPercent, setPartyHpPercent ] = useState(partyStartPercent);
|
const [ partyHpPercent, setPartyHpPercent ] = useState(100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startAnimation = setTimeout(() => {
|
const animationDurationMs = 5000;
|
||||||
setBossHpPercent(bossEndPercent);
|
const intervalMs = 50;
|
||||||
setPartyHpPercent(partyEndPercent);
|
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);
|
}, 200);
|
||||||
|
|
||||||
const revealResult = setTimeout(() => {
|
const revealTimeout = setTimeout(() => {
|
||||||
setPhase("result");
|
setPhase("result");
|
||||||
|
if (result.won) {
|
||||||
|
if (enableSounds) {
|
||||||
|
playSound("bossVictory");
|
||||||
|
}
|
||||||
|
if (enableNotifications) {
|
||||||
|
sendNotification("⚔️ Boss Defeated!", `You defeated ${bossName}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 5200);
|
}, 5200);
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
clearTimeout(startAnimation);
|
clearTimeout(startTimeout);
|
||||||
clearTimeout(revealResult);
|
clearTimeout(revealTimeout);
|
||||||
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [ bossEndPercent, partyEndPercent ]);
|
}, [
|
||||||
|
bossEndPercent,
|
||||||
|
bossName,
|
||||||
|
bossStartPercent,
|
||||||
|
enableNotifications,
|
||||||
|
enableSounds,
|
||||||
|
partyEndPercent,
|
||||||
|
result.won,
|
||||||
|
]);
|
||||||
|
|
||||||
let bossHpBarColour = "#c0392b";
|
const bossHpBarColour = getHpColour(bossHpPercent);
|
||||||
if (bossHpPercent > 50) {
|
const partyHpBarColour = getHpColour(partyHpPercent);
|
||||||
bossHpBarColour = "#e74c3c";
|
|
||||||
} else if (bossHpPercent > 25) {
|
|
||||||
bossHpBarColour = "#e67e22";
|
|
||||||
}
|
|
||||||
|
|
||||||
let partyHpBarColour = "#e74c3c";
|
|
||||||
if (partyHpPercent > 50) {
|
|
||||||
partyHpBarColour = "#27ae60";
|
|
||||||
} else if (partyHpPercent > 25) {
|
|
||||||
partyHpBarColour = "#f39c12";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay">
|
<div className="modal-overlay">
|
||||||
@@ -120,7 +170,6 @@ const BattleModal = ({
|
|||||||
className="hp-bar-fill"
|
className="hp-bar-fill"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: bossHpBarColour,
|
backgroundColor: bossHpBarColour,
|
||||||
transition: "width 5s ease-in-out",
|
|
||||||
width: `${bossHpPercent.toFixed(1)}%`,
|
width: `${bossHpPercent.toFixed(1)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -141,7 +190,6 @@ const BattleModal = ({
|
|||||||
className="hp-bar-fill party-hp"
|
className="hp-bar-fill party-hp"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: partyHpBarColour,
|
backgroundColor: partyHpBarColour,
|
||||||
transition: "width 5s ease-in-out",
|
|
||||||
width: `${partyHpPercent.toFixed(1)}%`,
|
width: `${partyHpPercent.toFixed(1)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1175,17 +1175,6 @@ export const GameProvider = ({
|
|||||||
return applyBossResult(previous, bossId, result);
|
return applyBossResult(previous, bossId, result);
|
||||||
});
|
});
|
||||||
setBattleResult({ bossName, result });
|
setBattleResult({ bossName, result });
|
||||||
if (result.won) {
|
|
||||||
if (enableSoundsReference.current) {
|
|
||||||
playSound("bossVictory");
|
|
||||||
}
|
|
||||||
if (enableNotificationsReference.current) {
|
|
||||||
sendNotification(
|
|
||||||
"⚔️ Boss Defeated!",
|
|
||||||
`You defeated ${bossName}!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).
|
}).
|
||||||
catch(() => {
|
catch(() => {
|
||||||
|
|
||||||
@@ -1785,14 +1774,6 @@ export const GameProvider = ({
|
|||||||
return applyBossResult(previous, bossId, result);
|
return applyBossResult(previous, bossId, result);
|
||||||
});
|
});
|
||||||
setBattleResult({ bossName: boss.name, result: 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 {
|
} catch {
|
||||||
// Silently ignore — server errors shouldn't crash the UI
|
// Silently ignore — server errors shouldn't crash the UI
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user