Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 5x 135x 135x 135x 135x 135x 135x 135x 675x 5x | /**
* StaminaBar Component - Segmented stamina display with Korean theming
*
* Displays player stamina with:
* - 5 segmented bars
* - Consistent cyan/blue theming
* - Pulse animation when stamina <20%
* - Korean/English bilingual labels
* - Numeric value display (e.g., "45/50")
* - Responsive sizing for mobile/tablet/desktop
* - Smooth transitions and glow effects
*
* Performance: Uses React.memo with shallow comparison for 60fps optimization.
* Note: React.memo uses shallow comparison by default, which works correctly
* for this component since all props are primitives (number, string, boolean).
* If object or function props are added in the future, consider adding a
* custom comparison function or using useCallback/useMemo for prop stability.
*/
import React, { useMemo } from "react";
import { KOREAN_COLORS, FONT_FAMILY } from "../../../../types/constants";
import { hexToRgbaString } from "../../../../utils/colorUtils";
import "./HUDAnimations.css";
export interface StaminaBarProps {
/** Current stamina value */
readonly current: number;
/** Maximum stamina capacity */
readonly max: number;
/** Player identifier for test ID */
readonly playerId: string;
/** Whether to use mobile-optimized sizing */
readonly isMobile: boolean;
}
/**
* StaminaBar - Segmented stamina display with Korean theming
* Performance optimized with React.memo
*/
export const StaminaBar: React.FC<StaminaBarProps> = React.memo(({
current,
max,
playerId,
isMobile,
}) => {
// Calculate stamina percentage
const staminaPercent = useMemo(
() => Math.max(0, Math.min(100, (current / max) * 100)),
[current, max]
);
const segments = 5;
const filledSegments = Math.ceil((staminaPercent / 100) * segments);
const shouldPulse = staminaPercent < 20;
// Responsive sizing with memoization
const layout = useMemo(() => ({
barWidth: isMobile ? 180 : 250,
barHeight: isMobile ? 10 : 12,
fontSize: isMobile ? 10 : 11,
padding: isMobile ? "6px 8px" : "8px 12px",
}), [isMobile]);
return (
<div
data-testid={`stamina-bar-${playerId}`}
role="progressbar"
aria-label="기력 | Stamina"
aria-valuenow={Math.ceil(current)}
aria-valuemin={0}
aria-valuemax={max}
aria-valuetext={`${Math.ceil(current)} out of ${max}`}
className="hud-animated"
style={{
width: `${layout.barWidth}px`,
padding: layout.padding,
backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),
borderRadius: "8px",
border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)}`,
boxShadow: `0 0 8px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.2)}`,
transition: "box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out",
}}
>
{/* Label and numeric display */}
<div
style={{
fontSize: `${layout.fontSize}px`,
color: hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1),
fontFamily: FONT_FAMILY.KOREAN,
marginBottom: "3px",
display: "flex",
justifyContent: "space-between",
fontWeight: "bold",
transition: "color 0.2s ease-in-out",
}}
>
<span>기력 | Stamina</span>
<span data-testid={`stamina-value-${playerId}`}>
{Math.ceil(current)}/{max}
</span>
</div>
{/* Segmented stamina bar */}
<div
style={{
display: "flex",
gap: "4px",
height: `${layout.barHeight}px`,
animation: shouldPulse ? "staminaPulse 0.8s ease-in-out infinite" : "none",
}}
>
{Array.from({ length: segments }).map((_, index) => (
<div
key={index}
data-testid={`stamina-segment-${playerId}-${index}`}
style={{
flex: 1,
backgroundColor:
index < filledSegments
? hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)
: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),
borderRadius: "2px",
transition: "background-color 0.2s ease-in-out",
boxShadow:
index < filledSegments
? `0 0 6px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.4)}`
: "none",
}}
/>
))}
</div>
</div>
);
});
StaminaBar.displayName = "StaminaBar";
export default StaminaBar;
|