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 | 4x 60x 30x 30x 30x 30x 30x 30x 30x 60x 60x 4x | /**
* StaminaWarning Component - Visual warning for low stamina
*
* Displays a yellow flashing indicator when stamina drops below critical threshold (20%).
* Uses CSS animation for attention-grabbing flash effect.
*
* NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.
* It does NOT use Html from drei - it's a standard React component.
*
* @module components/combat/StaminaWarning
* @category Combat UI
* @korean 체력경고
*/
import React, { useMemo } from "react";
import { KOREAN_COLORS } from "../../../../../types/constants";
export interface StaminaWarningProps {
/**
* Current stamina amount (0-100)
* @korean 체력량
*/
readonly stamina: number;
/**
* Mobile responsive mode (reduced flash intensity)
* @korean 모바일여부
*/
readonly isMobile: boolean;
}
/**
* StaminaWarning - Yellow flash warning for critical stamina depletion
*
* Renders a fullscreen flashing yellow border when stamina drops below 20%.
* Only visible when stamina is below 20. If stamina is 20 or higher, the component does not render.
* Uses CSS keyframe animation for fast attention-grabbing flash at 60fps.
*
* @example
* ```tsx
* <StaminaWarning stamina={15} isMobile={false} /> // Renders warning
* <StaminaWarning stamina={25} isMobile={false} /> // Does not render
* ```
*/
export const StaminaWarning: React.FC<StaminaWarningProps> = ({
stamina,
isMobile,
}) => {
const warningStyle = useMemo(() => {
// Only show when stamina is critically low
const criticalThreshold = 20;
Eif (stamina >= criticalThreshold) {
return null;
}
// Clamp stamina to 0-20 range for intensity calculation
const clampedStamina = Math.max(0, Math.min(criticalThreshold, stamina));
// Calculate urgency based on how low stamina is (20-0% -> 0-1)
const urgency = (criticalThreshold - clampedStamina) / criticalThreshold;
// Mobile uses thinner border
const borderWidth = isMobile ? "4px" : "6px";
// Use KOREAN_COLORS.WARNING_YELLOW constant
const rgb = KOREAN_COLORS.WARNING_YELLOW;
const warningColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${
rgb & 255
})`;
// Animation speed increases with urgency
const animationDuration = Math.max(0.6, 1.2 - urgency * 0.6);
return {
position: "fixed" as const,
inset: borderWidth,
pointerEvents: "none" as const,
border: `${borderWidth} solid ${warningColor}`,
boxShadow: `0 0 20px ${warningColor}`,
animation: `staminaFlash ${animationDuration}s ease-in-out infinite`,
transition: "border-color 0.3s ease-out",
zIndex: 85, // Below balance indicator but above game content
};
}, [stamina, isMobile]);
// Don't render if stamina is not critical
Eif (stamina >= 20 || !warningStyle) {
return null;
}
return (
<>
{/* CSS keyframe animation for flashing */}
<style>
{`
@keyframes staminaFlash {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
}
`}
</style>
<div
data-testid="stamina-warning"
style={warningStyle}
aria-hidden="true"
/>
</>
);
};
StaminaWarning.displayName = "StaminaWarning";
|