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 147 148 | 3x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 3x | /**
* BloodLossOverlayHtml Component - Visual warning for blood loss
*
* Displays a pulsing red overlay when blood loss exceeds critical threshold (50%).
* Uses CSS animation for smooth pulsing 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/BloodLossOverlayHtml
* @category Combat UI
* @korean 출혈오버레이
*/
import React, { useMemo } from "react";
import { KOREAN_COLORS } from "../../../../../types/constants";
export interface BloodLossOverlayProps {
/**
* Current blood loss amount (0-100)
* @korean 출혈량
*/
readonly bloodLoss: number;
/**
* Mobile responsive mode (reduced pulse intensity)
* @korean 모바일여부
*/
readonly isMobile: boolean;
}
/**
* BloodLossOverlayHtml - Pulsing red warning for critical blood loss
*
* Renders a fullscreen pulsing red overlay when blood loss is 50 or higher.
* Only visible when bloodLoss is 50 or above; does not render for values below 50.
* Uses CSS keyframe animation for smooth pulsing effect at 60fps.
*
* Optimized with React.memo for 60fps performance:
* - Prevents re-renders when blood loss hasn't changed significantly
* - Memoized style calculations
*
* Accessibility notes:
* - Purely decorative visual effect; no ARIA role or aria-live region is defined here
* - Typically rendered within an aria-hidden container so it is ignored by screen readers
* - Critical status announcements should be provided by separate, semantic HUD components
*
* @example
* ```tsx
* // Overlay is rendered (bloodLoss >= 50)
* <BloodLossOverlayHtml bloodLoss={75} isMobile={false} />
*
* // Overlay is not rendered (bloodLoss < 50)
* <BloodLossOverlayHtml bloodLoss={30} isMobile={false} />
* ```
*/
export const BloodLossOverlayHtml = React.memo<BloodLossOverlayProps>(
({ bloodLoss, isMobile }) => {
const overlayStyle = useMemo(() => {
// Only show when blood loss exceeds critical threshold
const criticalThreshold = 50;
Eif (bloodLoss < criticalThreshold) {
return null;
}
// Clamp blood loss to 50-100 range for intensity calculation
const clampedBloodLoss = Math.max(
criticalThreshold,
Math.min(100, bloodLoss)
);
// Calculate intensity based on blood loss (50-100% -> 0-1)
const intensity =
(clampedBloodLoss - criticalThreshold) / (100 - criticalThreshold);
// Mobile uses reduced intensity
const maxOpacity = isMobile ? 0.15 : 0.25;
const baseOpacity = intensity * maxOpacity;
// Use KOREAN_COLORS.BLOODLOSS_INDICATOR constant
const rgb = KOREAN_COLORS.BLOODLOSS_INDICATOR;
const bloodColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${
rgb & 255
})`;
return {
position: "fixed" as const,
inset: 0,
pointerEvents: "none" as const,
backgroundColor: bloodColor,
// Use CSS variable for dynamic animation
["--base-opacity"]: baseOpacity.toString(),
animation: "bloodLossPulse 1.5s ease-in-out infinite",
transition: "opacity 0.5s ease-out",
zIndex: 55, // Between pain vignette and consciousness blur
} as React.CSSProperties;
}, [bloodLoss, isMobile]);
// Don't render if blood loss is below threshold
Eif (bloodLoss < 50 || !overlayStyle) {
return null;
}
// Purely visual effect overlay; intentionally hidden from assistive technology via aria-hidden and does not use ARIA roles or live regions
return (
<>
{/* CSS keyframe animation for pulsing with CSS variables */}
<style>
{`
@keyframes bloodLossPulse {
0%, 100% {
opacity: var(--base-opacity);
}
50% {
opacity: calc(var(--base-opacity) * 1.5);
}
}
`}
</style>
<div
data-testid="bloodloss-overlay"
style={overlayStyle}
aria-hidden="true"
/>
</>
);
},
(prevProps, nextProps) => {
// Only re-render if blood loss crosses critical threshold or changes significantly
const wasCritical = prevProps.bloodLoss >= 50;
const isCritical = nextProps.bloodLoss >= 50;
// If neither are critical, no need to re-render
if (!wasCritical && !isCritical) return true;
// If crossing threshold, need to re-render
if (wasCritical !== isCritical) return false;
// If both critical, only re-render if significant change (5+ points)
return (
Math.abs(prevProps.bloodLoss - nextProps.bloodLoss) < 5 &&
prevProps.isMobile === nextProps.isMobile
);
},
);
BloodLossOverlayHtml.displayName = "BloodLossOverlayHtml";
|