All files / components/combat/components TechniqueBar.tsx

94.73% Statements 18/19
70% Branches 14/20
83.33% Functions 5/6
94.73% Lines 18/19

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                                                                                                                      2x                       41x 41x 41x 41x   41x   41x                     41x 392x       41x 392x 392x       41x   41x                                     392x 392x 392x   392x                                                                                        
/**
 * TechniqueBar Component
 *
 * **Korean**: 기술 바 컴포넌트 (Technique Bar Component)
 *
 * Horizontal bar displaying 3-5 technique cards at the bottom-center of combat HUD.
 * Manages technique selection, resource availability, and cooldown states.
 *
 * @module components/combat/components/TechniqueBar
 * @category Combat UI
 * @korean 기술바
 */
 
import React, { useMemo } from "react";
import { PlayerState } from "../../../systems/player";
import { Technique } from "../../../types";
import { TechniqueCard } from "./TechniqueCard";
 
/**
 * Props for TechniqueBar component.
 */
export interface TechniqueBarProps {
  /** Available techniques for player */
  readonly techniques: readonly Technique[];
 
  /** Player state with resources */
  readonly player: PlayerState;
 
  /** Index of currently selected technique */
  readonly selectedIndex: number;
 
  /** Active cooldowns map (techniqueId -> remaining ms) */
  readonly cooldowns: ReadonlyMap<string, number>;
 
  /** Callback when technique is selected */
  readonly onTechniqueSelect: (index: number) => void;
 
  /** Callback when hovering technique (for tooltip) */
  readonly onTechniqueHover: (technique: Technique | null) => void;
 
  /** Whether rendering for mobile device */
  readonly isMobile: boolean;
 
  /** Screen width for positioning */
  readonly screenWidth: number;
 
  /** Screen height for positioning */
  readonly screenHeight: number;
}
 
/**
 * TechniqueBar Component
 *
 * Displays a horizontal bar of technique cards positioned at the bottom-center
 * of the combat screen. Each card shows technique details and availability.
 *
 * @param props - Component props
 * @returns TechniqueBar component
 */
export const TechniqueBar: React.FC<TechniqueBarProps> = ({
  techniques,
  player,
  selectedIndex,
  cooldowns,
  onTechniqueSelect,
  onTechniqueHover,
  isMobile,
  screenWidth,
  screenHeight,
}) => {
  // Calculate card sizing and spacing
  const layout = useMemo(() => {
    const cardWidth = isMobile ? 70 : 90;
    const cardHeight = isMobile ? 80 : 100;
    const gap = isMobile ? 8 : 12;
    const totalWidth =
      techniques.length * cardWidth + (techniques.length - 1) * gap;
 
    return {
      cardWidth,
      cardHeight,
      gap,
      totalWidth,
      startX: (screenWidth - totalWidth) / 2,
      startY: screenHeight - cardHeight - (isMobile ? 100 : 120),
    };
  }, [techniques.length, isMobile, screenWidth, screenHeight]);
 
  // Check if player has sufficient resources for a technique
  const hasResources = (tech: Technique): boolean => {
    return player.stamina >= tech.staminaCost && player.ki >= tech.kiCost;
  };
 
  // Check if technique is available (has resources and not on cooldown)
  const isAvailable = (tech: Technique): boolean => {
    const onCooldown = (cooldowns.get(tech.id) ?? 0) > 0;
    return hasResources(tech) && !onCooldown;
  };
 
  // Calculate bottom position for proper placement
  const bottomOffset = isMobile ? 100 : 120;
 
  return (
    <>
      {/* Technique Bar Container - positioned at bottom center */}
      <div
        style={{
          position: "absolute",
          left: "50%",
          bottom: `${bottomOffset}px`,
          transform: "translateX(-50%)",
          width: `${layout.totalWidth}px`,
          height: `${layout.cardHeight}px`,
          display: "flex",
          gap: `${layout.gap}px`,
          pointerEvents: "auto",
          zIndex: 100,
        }}
        data-testid="technique-bar"
      >
        {techniques.map((technique, index) => {
          const cardX = index * (layout.cardWidth + layout.gap);
          const cooldownRemaining = cooldowns.get(technique.id) ?? 0;
          const available = isAvailable(technique);
 
          return (
            <div key={technique.id} data-testid={`technique-slot-${index}`}>
              <TechniqueCard
                technique={technique}
                isSelected={selectedIndex === index}
                isAvailable={available}
                staminaCost={technique.staminaCost}
                kiCost={technique.kiCost}
                remainingCooldown={cooldownRemaining}
                keyboardShortcut={technique.keyboardShortcut}
                onClick={() => onTechniqueSelect(index)}
                onHover={onTechniqueHover}
                isMobile={isMobile}
                position={{ x: cardX, y: 0 }}
              />
            </div>
          );
        })}
      </div>
 
      {/* Keyboard Hints (Optional) */}
      {!isMobile && (
        <div
          style={{
            position: "absolute",
            left: "50%",
            bottom: `${bottomOffset - layout.cardHeight - 20}px`,
            transform: "translateX(-50%)",
            width: `${layout.totalWidth}px`,
            textAlign: "center",
            fontSize: "11px",
            color: "#888",
            fontFamily: "monospace",
            pointerEvents: "none",
          }}
        >
          Press Q-P to use techniques
        </div>
      )}
    </>
  );
};
 
export default TechniqueBar;