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 | 5x 362x 362x 244x 244x 362x 246x 246x 246x 246x 246x 362x 353x 9x 362x 362x 362x 362x 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, KOREAN_COLORS } 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,
}) => {
const [currentTime, setCurrentTime] = useState(() => Date.now());
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(Date.now());
}, 100);
return () => clearInterval(interval);
}, []);
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]);
if (!breathingState.visible) {
return null;
}
const iconSize = isMobile ? 24 : 32;
const fontSize = isMobile ? 10 : 12;
const padding = isMobile ? "4px 8px" : "6px 12px";
const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);
return (
<div
data-testid="breathing-indicator"
style={{
display: "flex",
alignItems: "center",
gap: isMobile ? "6px" : "8px",
padding,
backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 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(KOREAN_COLORS.POSITIVE_GREEN, 0.8)
: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.6),
whiteSpace: "nowrap",
}}
>
{breathingState.isRecovering ? "회복중 | Recovering" : `${secondsRemaining}s`}
</div>
</div>
</div>
);
};
BreathingIndicator.displayName = "BreathingIndicator";
|