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 140 141 142 143 144 145 146 | 5x 380x 380x 256x 256x 380x 258x 258x 258x 258x 258x 380x 371x 9x 380x 380x 380x 380x 5x | /**
* BreathingIndicator Component - Visual feedback for breathing disruption status
*
* **Korean**: 호흡곤란 표시기
*
* Displays breathing difficulty with:
* - Color-coded lungs icon (🫁)
* - Bilingual label (Korean | English)
* - Time remaining until recovery
* - Pulsing animation based on severity
*/
import React, { useEffect, useMemo, useState } from "react";
import {
BreathingDisruptionSystem,
createBreathingIndicator,
} from "../../../../systems/breathing";
import { PlayerState } from "../../../../systems/player";
import { FONT_FAMILY } from "../../../../types/constants";
import { hexToRgbaString } from "../../../../utils/colorUtils";
import "./BreathingIndicator.css";
export interface BreathingIndicatorProps {
/** Player state to check for breathing disruption */
readonly player: PlayerState;
/** Whether to use mobile-optimized sizing */
readonly isMobile?: boolean;
}
/**
* BreathingIndicator - Shows breathing disruption status with Korean-English labels
*/
export const BreathingIndicator: React.FC<BreathingIndicatorProps> = ({
player,
isMobile = false,
}) => {
// Use state to trigger re-renders for timer updates
const [currentTime, setCurrentTime] = useState(() => Date.now());
// Update current time every 100ms to keep the timer accurate
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(Date.now());
}, 100);
return () => clearInterval(interval);
}, []);
// Get current breathing disruption state
const breathingState = useMemo(() => {
const level = BreathingDisruptionSystem.getCurrentLevel(player);
const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);
const timeRemaining = activeEffect
? Math.max(0, activeEffect.endTime - currentTime)
: 0;
const isRecovering = BreathingDisruptionSystem.canRecover(player);
return createBreathingIndicator(level, timeRemaining, isRecovering);
}, [player, currentTime]);
// Don't render if no breathing disruption
if (!breathingState.visible) {
return null;
}
// Responsive sizing
const iconSize = isMobile ? 24 : 32;
const fontSize = isMobile ? 10 : 12;
const padding = isMobile ? "4px 8px" : "6px 12px";
// Format time remaining
const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);
return (
<div
data-testid="breathing-indicator"
style={{
display: "flex",
alignItems: "center",
gap: isMobile ? "6px" : "8px",
padding,
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: "8px",
border: `2px solid ${hexToRgbaString(breathingState.color, breathingState.opacity)}`,
boxShadow: `0 0 10px ${hexToRgbaString(breathingState.color, 0.5)}`,
animation: "breathing-pulse 1s ease-in-out infinite",
pointerEvents: "none",
}}
>
{/* Lungs icon */}
<div
data-testid="breathing-icon"
style={{
fontSize: `${iconSize}px`,
lineHeight: 1,
transform: `scale(${breathingState.scale})`,
filter: `drop-shadow(0 0 6px ${hexToRgbaString(breathingState.color, 0.6)})`,
}}
>
{breathingState.icon}
</div>
{/* Label and time */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2px",
}}
>
{/* Bilingual label */}
<div
data-testid="breathing-label"
style={{
fontSize: `${fontSize}px`,
fontFamily: FONT_FAMILY.KOREAN,
fontWeight: "bold",
color: hexToRgbaString(breathingState.color, 1),
textShadow: `0 0 4px ${hexToRgbaString(breathingState.color, 0.8)}`,
whiteSpace: "nowrap",
}}
>
{breathingState.label.korean} | {breathingState.label.english}
</div>
{/* Time remaining */}
<div
data-testid="breathing-timer"
style={{
fontSize: `${fontSize - 2}px`,
fontFamily: FONT_FAMILY.KOREAN,
color: breathingState.isRecovering
? hexToRgbaString(0x00ff00, 0.8)
: hexToRgbaString(0xffffff, 0.6),
whiteSpace: "nowrap",
}}
>
{breathingState.isRecovering ? "회복중 | Recovering" : `${secondsRemaining}s`}
</div>
</div>
</div>
);
};
BreathingIndicator.displayName = "BreathingIndicator";
|