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;
|