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 | 3x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 3x | /**
* 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.
*
* Refactored to use useKoreanTheme for consistent styling.
*
* @module components/combat/StaminaWarning
* @category Combat UI
* @korean 체력경고
*/
import React, { useMemo } from "react";
import { useKoreanTheme } from "../../../../shared/base/useKoreanTheme";
import { hexToRgbaString } from "../../../../../utils/colorUtils";
export interface StaminaWarningProps {
/**
* Current stamina amount (0-100)
* @korean 체력량
*/
readonly stamina: number;
/**
* Mobile responsive mode (reduced flash intensity)
* @korean 모바일여부
*/
readonly isMobile: boolean;
/**
* Multiplier applied to the warning's visual weight (0.0-1.0).
*
* Used to soften the fullscreen flash when the 3D arena is already
* visually compressed (e.g. portrait mobile). Default is `1.0`.
*
* @korean 효과강도배수
*/
readonly intensityScale?: number;
}
/**
* 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.
* Uses useKoreanTheme for consistent color scheme.
*
* @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,
intensityScale = 1,
}) => {
const theme = useKoreanTheme({ variant: "danger", size: "md", 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;
// Attenuation (e.g. 0.5 on portrait mobile). Applied to the glow and
// a separate `borderColor` alpha, but the border itself always renders
// at full opacity so the warning frame stays visible.
const safeScale = Math.max(0, Math.min(1, intensityScale));
// Mobile uses thinner border
const borderWidth = isMobile ? "4px" : "6px";
// Border stays at full alpha so the warning outline is always visible,
// regardless of `intensityScale`.
const borderColor = hexToRgbaString(theme.colors.WARNING_YELLOW, 1);
// Glow is attenuated so the overall flash is subtler when requested.
const glowColor = hexToRgbaString(theme.colors.WARNING_YELLOW, safeScale);
// 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 ${borderColor}`,
boxShadow: `0 0 ${Math.round(20 * safeScale)}px ${glowColor}`,
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, intensityScale, theme.colors.WARNING_YELLOW]);
// 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";
|