All files / components/combat/components InputBufferDisplay.tsx

100% Statements 12/12
100% Branches 19/19
100% Functions 3/3
100% Lines 11/11

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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177                                                                                                    2x         72x 42x                               72x   11x 72x 72x 72x   72x                                                                             18x   18x                                                                                                                
/**
 * InputBufferDisplay - Input queue visualization
 * Shows queued combat actions for player feedback
 * 
 * @module components/combat/components/InputBufferDisplay
 * @category Combat UI
 * @korean 입력버퍼표시
 */
 
import { Html } from "@react-three/drei";
import React, { useMemo } from "react";
import { FONT_FAMILY, KOREAN_COLORS } from "../../../types/constants";
import { hexToRgbaString } from "../../../utils/colorUtils";
import { QueuedInput } from "../../../hooks/useKeyboardControls";
 
/**
 * Props for InputBufferDisplay component
 */
export interface InputBufferDisplayProps {
  /** Queued inputs to display */
  readonly queuedInputs: readonly QueuedInput[];
  /** Mobile layout flag */
  readonly isMobile?: boolean;
}
 
/**
 * InputBufferDisplay Component
 * 
 * Displays a list of recently queued combat inputs in the top-right corner.
 * Useful for showing input confirmation and debugging input lag issues.
 * 
 * Features:
 * - Displays up to 3 recent inputs
 * - Fades older inputs with reduced opacity
 * - Shows action name and key pressed
 * - Responsive mobile layout
 * - Korean cyberpunk styling
 * - Automatically clears after 2 seconds
 * 
 * @example
 * ```tsx
 * <InputBufferDisplay
 *   queuedInputs={queuedInputs}
 *   isMobile={isMobile}
 * />
 * ```
 * 
 * @public
 * @korean 입력버퍼표시
 */
export const InputBufferDisplay: React.FC<InputBufferDisplayProps> = ({
  queuedInputs,
  isMobile = false,
}) => {
  // Memoize animation styles to prevent redefinition on every render
  const animationStyles = useMemo(() => (
    <style>
      {`
        @keyframes slideIn {
          0% {
            opacity: 0;
            transform: translateX(10px);
          }
          100% {
            opacity: 1;
            transform: translateX(0);
          }
        }
      `}
    </style>
  ), []);
 
  if (queuedInputs.length === 0) return null;
 
  const fontSize = isMobile ? 10 : 12;
  const labelFontSize = isMobile ? 8 : 10;
  const top = isMobile ? "10px" : "20px";
  const right = isMobile ? "10px" : "20px";
 
  return (
    <Html fullscreen>
      <div
        data-testid="input-buffer-display"
        role="log"
        aria-live="polite"
        aria-label="Input queue"
        style={{
          position: "absolute",
          top,
          right,
          background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),
          border: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.6)}`,
          borderRadius: "4px",
          padding: isMobile ? "4px" : "8px",
          minWidth: isMobile ? "100px" : "150px",
          pointerEvents: "none",
          zIndex: 998,
          boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.2)}`,
        }}
      >
        {/* Title */}
        <div
          style={{
            fontSize: `${labelFontSize}px`,
            color: hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.9),
            fontFamily: FONT_FAMILY.KOREAN,
            marginBottom: "4px",
            fontWeight: "bold",
            textTransform: "uppercase",
          }}
        >
          Input Queue
        </div>
 
        {/* Input list */}
        <div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
          {queuedInputs.map((input, index) => {
            // Calculate opacity based on age (newer = more opaque)
            const opacity = 1 - index * 0.3;
 
            return (
              <div
                key={`${input.timestamp}-${input.key}`}
                data-testid={`queued-input-${index}`}
                style={{
                  fontSize: `${fontSize}px`,
                  color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, opacity),
                  fontFamily: FONT_FAMILY.KOREAN,
                  display: "flex",
                  justifyContent: "space-between",
                  alignItems: "center",
                  gap: "8px",
                  animation: index === 0 ? "slideIn 0.2s ease-out" : "none",
                }}
              >
                {/* Action name */}
                <span
                  style={{
                    flex: 1,
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                  }}
                >
                  {input.action}
                </span>
 
                {/* Key badge */}
                <span
                  style={{
                    padding: "2px 4px",
                    background: hexToRgbaString(
                      KOREAN_COLORS.ACCENT_BLUE,
                      0.2 * opacity
                    ),
                    border: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.5 * opacity)}`,
                    borderRadius: "2px",
                    fontSize: `${fontSize - 2}px`,
                    fontWeight: "bold",
                    minWidth: isMobile ? "20px" : "24px",
                    textAlign: "center",
                  }}
                >
                  {input.key.toUpperCase()}
                </span>
              </div>
            );
          })}
        </div>
 
        {/* CSS Animation - Memoized to prevent redefinition */}
        {animationStyles}
      </div>
    </Html>
  );
};