blacktrigram 0.7.11 → 0.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DATA_MODEL.md +347 -0
- package/lib/App.d.ts.map +1 -1
- package/lib/App2.js +0 -6
- package/lib/App2.js.map +1 -1
- package/lib/assets/index.css +102 -94
- package/lib/components/screens/combat/CombatScreen3D.d.ts +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +2 -2
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.d.ts.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js +22 -7
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.d.ts +2 -0
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.d.ts.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js +7 -4
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.d.ts.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js +15 -5
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +15 -16
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js +1 -2
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js +28 -24
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js +2 -4
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.d.ts.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js +3 -2
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +28 -30
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.d.ts.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js +4 -3
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.d.ts.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js +5 -2
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/components/TrigramSymbol3D.d.ts.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js +4 -4
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/shared/base/ResponsiveContainer.d.ts +6 -0
- package/lib/components/shared/base/ResponsiveContainer.d.ts.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +1 -2
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +1 -2
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js +5 -5
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -2
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.d.ts.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js +27 -30
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.d.ts.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +57 -59
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.d.ts +40 -0
- package/lib/components/shared/ui/BaseHUDContainer.d.ts.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js +40 -0
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/MobileHUDLayout.d.ts +13 -0
- package/lib/components/shared/ui/MobileHUDLayout.d.ts.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +10 -10
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.d.ts.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +57 -62
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js +7 -7
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VisualKeyboard3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/controls/components/VisualKeyboard3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAkB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VisualKeyboard3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/controls/components/VisualKeyboard3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAkB,MAAM,OAAO,CAAC;AAKvC;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;CACxD;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAyE5D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KOREAN_COLORS } from "../../../../types/constants/colors.js";
|
|
1
2
|
import { KEYBOARD_LAYOUT, filterKeysByCategory } from "../constants/ControlsConstants.js";
|
|
2
3
|
import Key3D from "./Key3D.js";
|
|
3
4
|
import { useMemo } from "react";
|
|
@@ -86,7 +87,7 @@ var VisualKeyboard3D = ({ pressedKeys, selectedTab }) => {
|
|
|
86
87
|
],
|
|
87
88
|
receiveShadow: true,
|
|
88
89
|
children: [/* @__PURE__ */ jsx("planeGeometry", { args: [12, 6] }), /* @__PURE__ */ jsx("meshStandardMaterial", {
|
|
89
|
-
color:
|
|
90
|
+
color: KOREAN_COLORS.ARENA_BACKGROUND,
|
|
90
91
|
metalness: .8,
|
|
91
92
|
roughness: .3,
|
|
92
93
|
opacity: .9,
|
|
@@ -97,8 +98,8 @@ var VisualKeyboard3D = ({ pressedKeys, selectedTab }) => {
|
|
|
97
98
|
args: [
|
|
98
99
|
12,
|
|
99
100
|
20,
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
KOREAN_COLORS.UI_STEEL_GRAY_DARK,
|
|
102
|
+
KOREAN_COLORS.UI_BACKGROUND_MEDIUM
|
|
102
103
|
],
|
|
103
104
|
position: [
|
|
104
105
|
0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VisualKeyboard3D.js","names":[],"sources":["../../../../../src/components/screens/controls/components/VisualKeyboard3D.tsx"],"sourcesContent":["/**\n * VisualKeyboard3D - 3D keyboard visualization with all keys\n * \n * Renders a complete 3D keyboard with keys filtered by selected category.\n * Uses grid layout with proper spacing and lighting for visual clarity.\n * \n * @module components/screens/controls/components\n */\n\nimport React, { useMemo } from \"react\";\nimport { filterKeysByCategory, KEYBOARD_LAYOUT, type KeyData } from \"../constants/ControlsConstants\";\nimport { Key3D } from \"./Key3D\";\n\n/**\n * Props for VisualKeyboard3D component\n */\nexport interface VisualKeyboard3DProps {\n /** Set of currently pressed key codes */\n readonly pressedKeys: Set<string>;\n /** Selected control category to filter keys */\n readonly selectedTab: 'combat' | 'movement' | 'system';\n}\n\n/**\n * VisualKeyboard3D Component\n * \n * 3D keyboard visualization showing all keys filtered by selected category.\n * Keys are positioned in a grid layout with proper spacing.\n * \n * @example\n * ```tsx\n * <Canvas>\n * <VisualKeyboard3D\n * pressedKeys={new Set(['Space', 'KeyW'])}\n * selectedTab=\"combat\"\n * />\n * </Canvas>\n * ```\n */\nexport const VisualKeyboard3D: React.FC<VisualKeyboard3DProps> = ({\n pressedKeys,\n selectedTab,\n}) => {\n // Filter keys by selected category\n const filteredKeys = useMemo<readonly KeyData[]>(() => {\n return filterKeysByCategory(KEYBOARD_LAYOUT, selectedTab);\n }, [selectedTab]);\n\n return (\n <group\n position={[0, -1, 0]}\n rotation={[-Math.PI / 6, 0, 0]}\n data-testid=\"visual-keyboard\"\n >\n {/* Ambient light for overall illumination */}\n <ambientLight intensity={0.4} />\n\n {/* Directional light for depth and shadows */}\n <directionalLight\n position={[5, 5, 5]}\n intensity={1.0}\n castShadow\n shadow-mapSize-width={2048}\n shadow-mapSize-height={2048}\n />\n\n {/* Additional directional light from the side */}\n <directionalLight\n position={[-3, 3, 2]}\n intensity={0.5}\n />\n\n {/* Point light for accent highlights */}\n <pointLight\n position={[0, 2, 2]}\n intensity={0.6}\n distance={10}\n decay={2}\n />\n\n {/* Render all filtered keys */}\n {filteredKeys.map((keyData) => (\n <Key3D\n key={keyData.code}\n keyData={keyData}\n isPressed={pressedKeys.has(keyData.code)}\n />\n ))}\n\n {/* Background plane for keyboard base */}\n <mesh\n position={[0, 0, -0.2]}\n receiveShadow\n >\n <planeGeometry args={[12, 6]} />\n <meshStandardMaterial\n color={
|
|
1
|
+
{"version":3,"file":"VisualKeyboard3D.js","names":[],"sources":["../../../../../src/components/screens/controls/components/VisualKeyboard3D.tsx"],"sourcesContent":["/**\n * VisualKeyboard3D - 3D keyboard visualization with all keys\n * \n * Renders a complete 3D keyboard with keys filtered by selected category.\n * Uses grid layout with proper spacing and lighting for visual clarity.\n * \n * @module components/screens/controls/components\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"../../../../types/constants/colors\";\nimport { filterKeysByCategory, KEYBOARD_LAYOUT, type KeyData } from \"../constants/ControlsConstants\";\nimport { Key3D } from \"./Key3D\";\n\n/**\n * Props for VisualKeyboard3D component\n */\nexport interface VisualKeyboard3DProps {\n /** Set of currently pressed key codes */\n readonly pressedKeys: Set<string>;\n /** Selected control category to filter keys */\n readonly selectedTab: 'combat' | 'movement' | 'system';\n}\n\n/**\n * VisualKeyboard3D Component\n * \n * 3D keyboard visualization showing all keys filtered by selected category.\n * Keys are positioned in a grid layout with proper spacing.\n * \n * @example\n * ```tsx\n * <Canvas>\n * <VisualKeyboard3D\n * pressedKeys={new Set(['Space', 'KeyW'])}\n * selectedTab=\"combat\"\n * />\n * </Canvas>\n * ```\n */\nexport const VisualKeyboard3D: React.FC<VisualKeyboard3DProps> = ({\n pressedKeys,\n selectedTab,\n}) => {\n // Filter keys by selected category\n const filteredKeys = useMemo<readonly KeyData[]>(() => {\n return filterKeysByCategory(KEYBOARD_LAYOUT, selectedTab);\n }, [selectedTab]);\n\n return (\n <group\n position={[0, -1, 0]}\n rotation={[-Math.PI / 6, 0, 0]}\n data-testid=\"visual-keyboard\"\n >\n {/* Ambient light for overall illumination */}\n <ambientLight intensity={0.4} />\n\n {/* Directional light for depth and shadows */}\n <directionalLight\n position={[5, 5, 5]}\n intensity={1.0}\n castShadow\n shadow-mapSize-width={2048}\n shadow-mapSize-height={2048}\n />\n\n {/* Additional directional light from the side */}\n <directionalLight\n position={[-3, 3, 2]}\n intensity={0.5}\n />\n\n {/* Point light for accent highlights */}\n <pointLight\n position={[0, 2, 2]}\n intensity={0.6}\n distance={10}\n decay={2}\n />\n\n {/* Render all filtered keys */}\n {filteredKeys.map((keyData) => (\n <Key3D\n key={keyData.code}\n keyData={keyData}\n isPressed={pressedKeys.has(keyData.code)}\n />\n ))}\n\n {/* Background plane for keyboard base */}\n <mesh\n position={[0, 0, -0.2]}\n receiveShadow\n >\n <planeGeometry args={[12, 6]} />\n <meshStandardMaterial\n color={KOREAN_COLORS.ARENA_BACKGROUND}\n metalness={0.8}\n roughness={0.3}\n opacity={0.9}\n transparent\n />\n </mesh>\n\n {/* Grid helper for visual reference (subtle) */}\n <gridHelper\n args={[12, 20, KOREAN_COLORS.UI_STEEL_GRAY_DARK, KOREAN_COLORS.UI_BACKGROUND_MEDIUM]}\n position={[0, 0, -0.19]}\n rotation={[0, 0, 0]}\n />\n </group>\n );\n};\n\nexport default VisualKeyboard3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAa,oBAAqD,EAChE,aACA,kBACI;CAEJ,MAAM,eAAe,cAAkC;AACrD,SAAO,qBAAqB,iBAAiB,YAAY;IACxD,CAAC,YAAY,CAAC;AAEjB,QACE,qBAAC,SAAD;EACE,UAAU;GAAC;GAAG;GAAI;GAAE;EACpB,UAAU;GAAC,CAAC,KAAK,KAAK;GAAG;GAAG;GAAE;EAC9B,eAAY;YAHd;GAME,oBAAC,gBAAD,EAAc,WAAW,IAAO,CAAA;GAGhC,oBAAC,oBAAD;IACE,UAAU;KAAC;KAAG;KAAG;KAAE;IACnB,WAAW;IACX,YAAA;IACA,wBAAsB;IACtB,yBAAuB;IACvB,CAAA;GAGF,oBAAC,oBAAD;IACE,UAAU;KAAC;KAAI;KAAG;KAAE;IACpB,WAAW;IACX,CAAA;GAGF,oBAAC,cAAD;IACE,UAAU;KAAC;KAAG;KAAG;KAAE;IACnB,WAAW;IACX,UAAU;IACV,OAAO;IACP,CAAA;GAGD,aAAa,KAAK,YACjB,oBAAC,OAAD;IAEW;IACT,WAAW,YAAY,IAAI,QAAQ,KAAK;IACxC,EAHK,QAAQ,KAGb,CACF;GAGF,qBAAC,QAAD;IACE,UAAU;KAAC;KAAG;KAAG;KAAK;IACtB,eAAA;cAFF,CAIE,oBAAC,iBAAD,EAAe,MAAM,CAAC,IAAI,EAAE,EAAI,CAAA,EAChC,oBAAC,wBAAD;KACE,OAAO,cAAc;KACrB,WAAW;KACX,WAAW;KACX,SAAS;KACT,aAAA;KACA,CAAA,CACG;;GAGP,oBAAC,cAAD;IACE,MAAM;KAAC;KAAI;KAAI,cAAc;KAAoB,cAAc;KAAqB;IACpF,UAAU;KAAC;KAAG;KAAG;KAAM;IACvB,UAAU;KAAC;KAAG;KAAG;KAAE;IACnB,CAAA;GACI"}
|
|
@@ -21,7 +21,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
21
21
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
22
|
import { Canvas } from "@react-three/fiber";
|
|
23
23
|
//#region src/components/screens/intro/IntroScreen3D.tsx
|
|
24
|
-
var APP_VERSION = "0.7.
|
|
24
|
+
var APP_VERSION = "0.7.13";
|
|
25
25
|
var MENU_ITEMS = [
|
|
26
26
|
{
|
|
27
27
|
mode: GameMode.VERSUS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuButtonsOverlayHtml.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/intro/components/MenuButtonsOverlayHtml.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAYpD,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;QACxB,IAAI,EAAE,QAAQ,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4CAA4C;IAC5C,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IAChD,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,qCAAqC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,yCAAyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"MenuButtonsOverlayHtml.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/intro/components/MenuButtonsOverlayHtml.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAYpD,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;QACxB,IAAI,EAAE,QAAQ,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4CAA4C;IAC5C,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IAChD,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,qCAAqC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,yCAAyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAuJlD,CAAC;AAEF,eAAe,WAAW,CAAC"}
|
|
@@ -59,7 +59,10 @@ var MenuButtons = ({ menuItems, selectedIndex, hoveredIndex, onModeSelect, onHov
|
|
|
59
59
|
buttonDefaultBg: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, .92),
|
|
60
60
|
buttonSelectedBorder: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),
|
|
61
61
|
buttonHoveredBorder: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, .8),
|
|
62
|
-
buttonDefaultBorder: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, .7)
|
|
62
|
+
buttonDefaultBorder: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, .7),
|
|
63
|
+
textSelected: `#${KOREAN_COLORS.UI_BACKGROUND_DARK.toString(16).padStart(6, "0")}`,
|
|
64
|
+
textHovered: `#${KOREAN_COLORS.ACCENT_GOLD.toString(16).padStart(6, "0")}`,
|
|
65
|
+
textDefault: `#${KOREAN_COLORS.TEXT_PRIMARY.toString(16).padStart(6, "0")}`
|
|
63
66
|
}), []);
|
|
64
67
|
const handleButtonClick = useCallback((mode) => {
|
|
65
68
|
UIHaptics.buttonTap();
|
|
@@ -112,7 +115,7 @@ var MenuButtons = ({ menuItems, selectedIndex, hoveredIndex, onModeSelect, onHov
|
|
|
112
115
|
fontFamily: FONT_FAMILY.KOREAN,
|
|
113
116
|
width: "100%",
|
|
114
117
|
height: `${buttonHeight}px`,
|
|
115
|
-
color: isSelected ?
|
|
118
|
+
color: isSelected ? colors.textSelected : isHovered ? colors.textHovered : colors.textDefault,
|
|
116
119
|
background: isSelected ? colors.buttonSelectedBg : isHovered ? colors.buttonHoveredBg : colors.buttonDefaultBg,
|
|
117
120
|
border: isSelected ? `3px solid ${colors.buttonSelectedBorder}` : isHovered ? `2px solid ${colors.buttonHoveredBorder}` : `2px solid ${colors.buttonDefaultBorder}`,
|
|
118
121
|
cursor: "pointer"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuButtonsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/intro/components/MenuButtonsOverlayHtml.tsx"],"sourcesContent":["/**\n * MenuButtons - Reusable menu button grid for IntroScreen\n * \n * Provides 2x2 grid (desktop) or column (mobile) layout for menu navigation.\n * Extracted from MenuSectionOverlayHtml to reduce code duplication.\n * \n * @module components/screens/intro\n * @category Intro UI\n * @korean 메뉴버튼\n */\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { GameMode } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { UIHaptics } from \"../../../../utils/hapticFeedback\";\nimport {\n getButtonVisualEffectsOnly,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport { getMobileKoreanFontSize } from \"../../../../utils/mobileUIUtils\";\nimport {\n getKoreanFontOptimization,\n} from \"../../../../utils/visualEffects\";\n\nexport interface MenuButtonsProps {\n /** Array of menu items to display */\n readonly menuItems: Array<{\n mode: GameMode;\n korean: string;\n english: string;\n }>;\n /** Currently selected menu item index */\n readonly selectedIndex: number;\n /** Index of currently hovered menu item (null if none) */\n readonly hoveredIndex: number | null;\n /** Callback when a menu item is selected */\n readonly onModeSelect: (mode: GameMode) => void;\n /** Callback when hover state changes */\n readonly onHoverChange: (index: number | null) => void;\n /** Callback to play sound effects */\n readonly onPlaySFX?: (sound: string) => void;\n /** Screen width for responsive sizing */\n readonly width?: number;\n /** Whether on mobile device (for haptics) */\n readonly isMobile?: boolean;\n}\n\n/**\n * MenuButtons Component\n * \n * Displays menu navigation buttons with:\n * - 2x2 grid layout on larger screens\n * - Column layout on small screens\n * - Selected/hovered state visualization\n * - Korean bilingual text\n * - Haptic feedback support\n * \n * This component delegates to inline button elements with custom styling\n * since BaseButtonOverlayHtml doesn't support the complex selection state\n * and color transitions needed for menu navigation.\n * \n * Reduces code duplication by 62 lines (MenuSectionOverlayHtml: 372 → 310)\n * \n * @example\n * ```tsx\n * <MenuButtons\n * menuItems={MENU_ITEMS}\n * selectedIndex={0}\n * hoveredIndex={null}\n * onModeSelect={(mode) => handleModeSelect(mode)}\n * onHoverChange={(idx) => setHovered(idx)}\n * width={800}\n * />\n * ```\n */\nexport const MenuButtons: React.FC<MenuButtonsProps> = ({\n menuItems,\n selectedIndex,\n hoveredIndex,\n onModeSelect,\n onHoverChange,\n onPlaySFX,\n width = 800,\n isMobile: _isMobile = false, // Prefix with _ to indicate intentionally unused\n}) => {\n // Responsive sizing based on screen width\n const isSmallScreen = width < 768;\n const useGridLayout = !isSmallScreen;\n const buttonHeight = isSmallScreen ? 44 : 40;\n const buttonFontSize = isSmallScreen\n ? getMobileKoreanFontSize(\"SMALL\", width ?? 375)\n : 13;\n const buttonGap = isSmallScreen ? 6 : 8;\n\n // Memoize button state colors\n const colors = useMemo(\n () => ({\n buttonSelectedBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.98),\n buttonHoveredBg: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_LIGHT, 0.92),\n buttonDefaultBg: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.92,\n ),\n buttonSelectedBorder: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 1.0,\n ),\n buttonHoveredBorder: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n buttonDefaultBorder: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7),\n }),\n [],\n );\n\n const handleButtonClick = useCallback(\n (mode: GameMode) => {\n UIHaptics.buttonTap();\n onModeSelect(mode);\n onPlaySFX?.(\"menu_select\");\n },\n [onModeSelect, onPlaySFX],\n );\n\n const handleButtonHover = useCallback(\n (index: number, isHovering: boolean) => {\n const newIndex = isHovering ? index : null;\n onHoverChange(newIndex);\n if (isHovering) {\n UIHaptics.menuHover();\n onPlaySFX?.(\"menu_hover\");\n }\n },\n [onHoverChange, onPlaySFX],\n );\n\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: useGridLayout ? \"1fr 1fr\" : \"1fr\",\n gap: `${buttonGap}px`,\n width: \"100%\",\n }}\n data-testid=\"main-menu-buttons\"\n >\n {menuItems.map((item, index) => {\n const isSelected = selectedIndex === index;\n const isHovered = hoveredIndex === index;\n\n // Get only visual effects (glow, transitions, transforms)\n const visualEffects = getButtonVisualEffectsOnly({\n variant: \"primary\",\n isHovered,\n isPressed: false,\n isFocused: false,\n glowIntensity: isSelected\n ? \"medium\"\n : isHovered\n ? \"medium\"\n : \"subtle\",\n hoverAnimation: \"combined\",\n });\n\n return (\n <button\n key={item.mode}\n onClick={() => handleButtonClick(item.mode)}\n onMouseEnter={() => handleButtonHover(index, true)}\n onMouseLeave={() => handleButtonHover(index, false)}\n onFocus={(e) => {\n e.currentTarget.style.outline = `3px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}`;\n e.currentTarget.style.outlineOffset = \"2px\";\n }}\n onBlur={(e) => {\n e.currentTarget.style.outline = \"none\";\n }}\n aria-label={`${item.korean} (${item.english})`}\n aria-selected={isSelected}\n role=\"menuitem\"\n style={{\n ...visualEffects,\n ...getKoreanFontOptimization(\n buttonFontSize,\n isSelected ? \"bold\" : \"normal\",\n ),\n fontFamily: FONT_FAMILY.KOREAN,\n width: \"100%\",\n height: `${buttonHeight}px`,\n // Menu-specific color, background, and border\n color: isSelected\n ? `#${KOREAN_COLORS.UI_BACKGROUND_DARK.toString(16).padStart(\n 6,\n \"0\",\n )}`\n : isHovered\n ? `#${KOREAN_COLORS.ACCENT_GOLD.toString(16).padStart(\n 6,\n \"0\",\n )}`\n : `#${KOREAN_COLORS.TEXT_PRIMARY.toString(16).padStart(\n 6,\n \"0\",\n )}`,\n background: isSelected\n ? colors.buttonSelectedBg\n : isHovered\n ? colors.buttonHoveredBg\n : colors.buttonDefaultBg,\n border: isSelected\n ? `3px solid ${colors.buttonSelectedBorder}`\n : isHovered\n ? `2px solid ${colors.buttonHoveredBorder}`\n : `2px solid ${colors.buttonDefaultBorder}`,\n cursor: \"pointer\",\n }}\n data-testid={`menu-item-${item.mode}`}\n >\n {/* Add test ID aliases for backward compatibility */}\n {item.mode === GameMode.TRAINING && (\n <span\n data-testid=\"training-button\"\n style={{ display: \"none\" }}\n />\n )}\n {item.mode === GameMode.VERSUS && (\n <span data-testid=\"combat-button\" style={{ display: \"none\" }} />\n )}\n {item.korean} ({item.english})\n </button>\n );\n })}\n </div>\n );\n};\n\nexport default MenuButtons;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MenuButtonsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/intro/components/MenuButtonsOverlayHtml.tsx"],"sourcesContent":["/**\n * MenuButtons - Reusable menu button grid for IntroScreen\n * \n * Provides 2x2 grid (desktop) or column (mobile) layout for menu navigation.\n * Extracted from MenuSectionOverlayHtml to reduce code duplication.\n * \n * @module components/screens/intro\n * @category Intro UI\n * @korean 메뉴버튼\n */\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { GameMode } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { UIHaptics } from \"../../../../utils/hapticFeedback\";\nimport {\n getButtonVisualEffectsOnly,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport { getMobileKoreanFontSize } from \"../../../../utils/mobileUIUtils\";\nimport {\n getKoreanFontOptimization,\n} from \"../../../../utils/visualEffects\";\n\nexport interface MenuButtonsProps {\n /** Array of menu items to display */\n readonly menuItems: Array<{\n mode: GameMode;\n korean: string;\n english: string;\n }>;\n /** Currently selected menu item index */\n readonly selectedIndex: number;\n /** Index of currently hovered menu item (null if none) */\n readonly hoveredIndex: number | null;\n /** Callback when a menu item is selected */\n readonly onModeSelect: (mode: GameMode) => void;\n /** Callback when hover state changes */\n readonly onHoverChange: (index: number | null) => void;\n /** Callback to play sound effects */\n readonly onPlaySFX?: (sound: string) => void;\n /** Screen width for responsive sizing */\n readonly width?: number;\n /** Whether on mobile device (for haptics) */\n readonly isMobile?: boolean;\n}\n\n/**\n * MenuButtons Component\n * \n * Displays menu navigation buttons with:\n * - 2x2 grid layout on larger screens\n * - Column layout on small screens\n * - Selected/hovered state visualization\n * - Korean bilingual text\n * - Haptic feedback support\n * \n * This component delegates to inline button elements with custom styling\n * since BaseButtonOverlayHtml doesn't support the complex selection state\n * and color transitions needed for menu navigation.\n * \n * Reduces code duplication by 62 lines (MenuSectionOverlayHtml: 372 → 310)\n * \n * @example\n * ```tsx\n * <MenuButtons\n * menuItems={MENU_ITEMS}\n * selectedIndex={0}\n * hoveredIndex={null}\n * onModeSelect={(mode) => handleModeSelect(mode)}\n * onHoverChange={(idx) => setHovered(idx)}\n * width={800}\n * />\n * ```\n */\nexport const MenuButtons: React.FC<MenuButtonsProps> = ({\n menuItems,\n selectedIndex,\n hoveredIndex,\n onModeSelect,\n onHoverChange,\n onPlaySFX,\n width = 800,\n isMobile: _isMobile = false, // Prefix with _ to indicate intentionally unused\n}) => {\n // Responsive sizing based on screen width\n const isSmallScreen = width < 768;\n const useGridLayout = !isSmallScreen;\n const buttonHeight = isSmallScreen ? 44 : 40;\n const buttonFontSize = isSmallScreen\n ? getMobileKoreanFontSize(\"SMALL\", width ?? 375)\n : 13;\n const buttonGap = isSmallScreen ? 6 : 8;\n\n // Memoize button state colors\n const colors = useMemo(\n () => ({\n buttonSelectedBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.98),\n buttonHoveredBg: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_LIGHT, 0.92),\n buttonDefaultBg: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.92,\n ),\n buttonSelectedBorder: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 1.0,\n ),\n buttonHoveredBorder: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n buttonDefaultBorder: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7),\n textSelected: `#${KOREAN_COLORS.UI_BACKGROUND_DARK.toString(16).padStart(6, \"0\")}`,\n textHovered: `#${KOREAN_COLORS.ACCENT_GOLD.toString(16).padStart(6, \"0\")}`,\n textDefault: `#${KOREAN_COLORS.TEXT_PRIMARY.toString(16).padStart(6, \"0\")}`,\n }),\n [],\n );\n\n const handleButtonClick = useCallback(\n (mode: GameMode) => {\n UIHaptics.buttonTap();\n onModeSelect(mode);\n onPlaySFX?.(\"menu_select\");\n },\n [onModeSelect, onPlaySFX],\n );\n\n const handleButtonHover = useCallback(\n (index: number, isHovering: boolean) => {\n const newIndex = isHovering ? index : null;\n onHoverChange(newIndex);\n if (isHovering) {\n UIHaptics.menuHover();\n onPlaySFX?.(\"menu_hover\");\n }\n },\n [onHoverChange, onPlaySFX],\n );\n\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: useGridLayout ? \"1fr 1fr\" : \"1fr\",\n gap: `${buttonGap}px`,\n width: \"100%\",\n }}\n data-testid=\"main-menu-buttons\"\n >\n {menuItems.map((item, index) => {\n const isSelected = selectedIndex === index;\n const isHovered = hoveredIndex === index;\n\n // Get only visual effects (glow, transitions, transforms)\n const visualEffects = getButtonVisualEffectsOnly({\n variant: \"primary\",\n isHovered,\n isPressed: false,\n isFocused: false,\n glowIntensity: isSelected\n ? \"medium\"\n : isHovered\n ? \"medium\"\n : \"subtle\",\n hoverAnimation: \"combined\",\n });\n\n return (\n <button\n key={item.mode}\n onClick={() => handleButtonClick(item.mode)}\n onMouseEnter={() => handleButtonHover(index, true)}\n onMouseLeave={() => handleButtonHover(index, false)}\n onFocus={(e) => {\n e.currentTarget.style.outline = `3px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}`;\n e.currentTarget.style.outlineOffset = \"2px\";\n }}\n onBlur={(e) => {\n e.currentTarget.style.outline = \"none\";\n }}\n aria-label={`${item.korean} (${item.english})`}\n aria-selected={isSelected}\n role=\"menuitem\"\n style={{\n ...visualEffects,\n ...getKoreanFontOptimization(\n buttonFontSize,\n isSelected ? \"bold\" : \"normal\",\n ),\n fontFamily: FONT_FAMILY.KOREAN,\n width: \"100%\",\n height: `${buttonHeight}px`,\n // Menu-specific color, background, and border\n color: isSelected\n ? colors.textSelected\n : isHovered\n ? colors.textHovered\n : colors.textDefault,\n background: isSelected\n ? colors.buttonSelectedBg\n : isHovered\n ? colors.buttonHoveredBg\n : colors.buttonDefaultBg,\n border: isSelected\n ? `3px solid ${colors.buttonSelectedBorder}`\n : isHovered\n ? `2px solid ${colors.buttonHoveredBorder}`\n : `2px solid ${colors.buttonDefaultBorder}`,\n cursor: \"pointer\",\n }}\n data-testid={`menu-item-${item.mode}`}\n >\n {/* Add test ID aliases for backward compatibility */}\n {item.mode === GameMode.TRAINING && (\n <span\n data-testid=\"training-button\"\n style={{ display: \"none\" }}\n />\n )}\n {item.mode === GameMode.VERSUS && (\n <span data-testid=\"combat-button\" style={{ display: \"none\" }} />\n )}\n {item.korean} ({item.english})\n </button>\n );\n })}\n </div>\n );\n};\n\nexport default MenuButtons;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrigramSymbol3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/philosophy/components/TrigramSymbol3D.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,
|
|
1
|
+
{"version":3,"file":"TrigramSymbol3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/philosophy/components/TrigramSymbol3D.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA+J1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -131,8 +131,8 @@ var FootworkDrillsOverlayHtml = React.memo(({ currentDrill, onDrillChange, curre
|
|
|
131
131
|
borderRadius: "12px",
|
|
132
132
|
padding: isMobile ? "10px" : "12px",
|
|
133
133
|
fontFamily: FONT_FAMILY.KOREAN,
|
|
134
|
-
color:
|
|
135
|
-
boxShadow:
|
|
134
|
+
color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),
|
|
135
|
+
boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, .5)}`
|
|
136
136
|
},
|
|
137
137
|
"data-testid": "footwork-drills-html",
|
|
138
138
|
children: [
|
|
@@ -228,7 +228,7 @@ var FootworkDrillsOverlayHtml = React.memo(({ currentDrill, onDrillChange, curre
|
|
|
228
228
|
fontSize: isMobile ? "9px" : "10px",
|
|
229
229
|
fontFamily: FONT_FAMILY.KOREAN,
|
|
230
230
|
background: isActive && index === currentStep ? hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, .9) : index < currentStep ? hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, .6) : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, .7),
|
|
231
|
-
color: isActive && index === currentStep ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1) :
|
|
231
|
+
color: isActive && index === currentStep ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1) : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),
|
|
232
232
|
border: `1px solid ${hexToRgbaString(isActive && index === currentStep ? KOREAN_COLORS.PRIMARY_CYAN : KOREAN_COLORS.UI_BORDER, .8)}`,
|
|
233
233
|
borderRadius: "4px",
|
|
234
234
|
fontWeight: isActive && index === currentStep ? "bold" : "normal"
|
|
@@ -263,7 +263,7 @@ var FootworkDrillsOverlayHtml = React.memo(({ currentDrill, onDrillChange, curre
|
|
|
263
263
|
fontFamily: FONT_FAMILY.KOREAN,
|
|
264
264
|
fontWeight: "bold",
|
|
265
265
|
background: isActive ? hexToRgbaString(KOREAN_COLORS.ACCENT_RED, .9) : hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, .9),
|
|
266
|
-
color:
|
|
266
|
+
color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),
|
|
267
267
|
border: "none",
|
|
268
268
|
borderRadius: "8px",
|
|
269
269
|
cursor: "pointer",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FootworkDrillsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/FootworkDrillsOverlayHtml.tsx"],"sourcesContent":["/**\n * FootworkDrillsOverlayHtml - Training component for footwork drills\n * \n * Provides specialized footwork training exercises for Korean martial arts\n * footwork patterns (보법, Bobeop).\n * \n * @module components/screens/training/components/FootworkDrillsOverlayHtml\n * @category Training Components\n * @korean 보법훈련컴포넌트\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Footwork drill types for training\n * \n * @korean 보법훈련타입\n */\nexport type FootworkDrill = \n | \"circular_left\" // 원형보 좌 - Circle left around target\n | \"circular_right\" // 원형보 우 - Circle right around target\n | \"pivot_combo\" // 축족회전 - Pivot left-right combo\n | \"triangle_step\" // 삼각보법 - Triangle stepping pattern\n | \"slide_drill\" // 미끄럼보 - Four-direction slide drill\n | \"shuffle_practice\" // 섞음보 - Quick shuffle adjustments\n | \"free_practice\"; // 자유 연습 - Free practice mode\n\n/**\n * Drill information with Korean terminology\n * \n * @korean 훈련정보\n */\nconst DRILL_INFO: Record<FootworkDrill, { \n korean: string; \n english: string; \n description: string;\n pattern: string[];\n keyHints: string;\n}> = {\n circular_left: {\n korean: \"원형보 좌회전\",\n english: \"Circular Left\",\n description: \"원형보 좌측 | Circle stepping left\",\n pattern: [\"Ctrl+A\", \"Ctrl+A\", \"Ctrl+A\", \"Ctrl+A\"],\n keyHints: \"Hold Ctrl+A to circle left\",\n },\n circular_right: {\n korean: \"원형보 우회전\",\n english: \"Circular Right\",\n description: \"원형보 우측 | Circle stepping right\",\n pattern: [\"Ctrl+D\", \"Ctrl+D\", \"Ctrl+D\", \"Ctrl+D\"],\n keyHints: \"Hold Ctrl+D to circle right\",\n },\n pivot_combo: {\n korean: \"축족회전 연속\",\n english: \"Pivot Combo\",\n description: \"좌우 연속 회전 | Continuous pivot rotations\",\n pattern: [\"Shift+Ctrl+A\", \"Shift+Ctrl+D\", \"Shift+Ctrl+A\", \"Shift+Ctrl+D\"],\n keyHints: \"Alternate Shift+Ctrl+A/D\",\n },\n triangle_step: {\n korean: \"삼각보법\",\n english: \"Triangle Step\",\n description: \"삼각형 발놀림 | Triangle footwork pattern\",\n pattern: [\"Ctrl+W\", \"Shift+Ctrl+D\", \"Ctrl+S\", \"Shift+Ctrl+A\"],\n keyHints: \"Forward → Pivot → Back → Pivot\",\n },\n slide_drill: {\n korean: \"미끄럼보 사방\",\n english: \"Slide Drill\",\n description: \"사방 미끄럼 | Four-direction slides\",\n pattern: [\"Ctrl+W\", \"Alt+D\", \"Ctrl+S\", \"Alt+A\"],\n keyHints: \"Slide in all four directions\",\n },\n shuffle_practice: {\n korean: \"섞음보 연습\",\n english: \"Shuffle Practice\",\n description: \"빠른 조정 | Quick micro-adjustments\",\n pattern: [\"Shift+Ctrl+W\", \"Shift+Ctrl+W\", \"Shift+Ctrl+W\"],\n keyHints: \"Rapid Shift+Ctrl+W/S\",\n },\n free_practice: {\n korean: \"자유 연습\",\n english: \"Free Practice\",\n description: \"자유 보법 | Free footwork exploration\",\n pattern: [],\n keyHints: \"Use any footwork combination\",\n },\n};\n\n/**\n * Props for FootworkDrillsOverlayHtml component\n */\nexport interface FootworkDrillsOverlayHtmlProps {\n /** Currently selected drill */\n readonly currentDrill: FootworkDrill;\n /** Callback when drill changes */\n readonly onDrillChange: (drill: FootworkDrill) => void;\n /** Current step in drill pattern (0-based) */\n readonly currentStep: number;\n /** Callback when drill step completes (optional, not yet implemented) */\n readonly onStepComplete?: () => void;\n /** Whether drill is currently active */\n readonly isActive: boolean;\n /** Callback to start/stop drill */\n readonly onToggleActive: () => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * FootworkDrillsOverlayHtml Component\n * \n * Provides UI for footwork training drills with step-by-step guidance\n * and Korean martial arts terminology.\n * \n * @korean 보법훈련UI컴포넌트\n */\nexport const FootworkDrillsOverlayHtml = React.memo<FootworkDrillsOverlayHtmlProps>(\n ({\n currentDrill,\n onDrillChange,\n currentStep,\n // onStepComplete, // TODO: Use this for drill pattern validation\n isActive,\n onToggleActive,\n isMobile,\n }) => {\n const drillInfo = DRILL_INFO[currentDrill];\n const [showInstructions, setShowInstructions] = useState(true);\n\n // Auto-hide instructions after 5 seconds when drill is active\n useEffect(() => {\n if (isActive && showInstructions) {\n const timer = setTimeout(() => setShowInstructions(false), 5000);\n return () => clearTimeout(timer);\n }\n }, [isActive, showInstructions]);\n\n const handleDrillSelect = useCallback((drill: FootworkDrill) => {\n onDrillChange(drill);\n setShowInstructions(true);\n }, [onDrillChange]);\n\n const panelWidth = isMobile ? 280 : 340;\n const buttonFontSize = isMobile ? \"10px\" : \"11px\";\n const titleFontSize = isMobile ? \"13px\" : \"15px\";\n\n return (\n <div\n style={{\n width: `${panelWidth}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: \"#ffffff\",\n boxShadow: \"0 4px 20px rgba(0, 0, 0, 0.5)\",\n }}\n data-testid=\"footwork-drills-html\"\n >\n {/* Header */}\n <div style={{ marginBottom: \"12px\", textAlign: \"center\" }}>\n <div\n style={{\n fontSize: titleFontSize,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n marginBottom: \"4px\",\n }}\n >\n 보법 훈련 | Footwork Drills\n </div>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n }}\n >\n {drillInfo.korean} | {drillInfo.english}\n </div>\n </div>\n\n {/* Drill Selection Grid */}\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: isMobile ? \"1fr 1fr\" : \"1fr 1fr 1fr\",\n gap: \"6px\",\n marginBottom: \"12px\",\n }}\n >\n {(Object.keys(DRILL_INFO) as FootworkDrill[]).map((drill) => (\n <button\n key={drill}\n onClick={() => handleDrillSelect(drill)}\n style={{\n padding: isMobile ? \"6px 4px\" : \"8px 6px\",\n fontSize: buttonFontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: currentDrill === drill ? \"bold\" : \"normal\",\n background: \n currentDrill === drill\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.8),\n color: \n currentDrill === drill\n ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n border: `1px solid ${hexToRgbaString(\n currentDrill === drill\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.UI_BORDER,\n 0.6\n )}`,\n borderRadius: \"6px\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n lineHeight: 1.2,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n {DRILL_INFO[drill].korean.split(\" \")[0]}\n </button>\n ))}\n </div>\n\n {/* Drill Description */}\n <div\n style={{\n padding: \"8px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.6),\n borderRadius: \"6px\",\n marginBottom: \"10px\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n textAlign: \"center\",\n }}\n >\n {drillInfo.description}\n </div>\n\n {/* Pattern Steps (if drill has pattern) */}\n {drillInfo.pattern.length > 0 && (\n <div style={{ marginBottom: \"10px\" }}>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n marginBottom: \"6px\",\n textAlign: \"center\",\n }}\n >\n Pattern Steps:\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"center\",\n gap: \"4px\",\n flexWrap: \"wrap\",\n }}\n >\n {drillInfo.pattern.map((step, index) => (\n <div\n key={index}\n style={{\n padding: \"4px 8px\",\n fontSize: isMobile ? \"9px\" : \"10px\",\n fontFamily: FONT_FAMILY.KOREAN,\n background: \n isActive && index === currentStep\n ? hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)\n : index < currentStep\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.7),\n color: \n isActive && index === currentStep\n ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1)\n : \"#ffffff\",\n border: `1px solid ${hexToRgbaString(\n isActive && index === currentStep\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.UI_BORDER,\n 0.8\n )}`,\n borderRadius: \"4px\",\n fontWeight: isActive && index === currentStep ? \"bold\" : \"normal\",\n }}\n >\n {index + 1}. {step}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Key Hints */}\n {showInstructions && (\n <div\n style={{\n padding: \"8px\",\n background: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n marginBottom: \"10px\",\n fontSize: isMobile ? \"9px\" : \"10px\",\n textAlign: \"center\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n 💡 {drillInfo.keyHints}\n </div>\n )}\n\n {/* Start/Stop Button */}\n <button\n onClick={onToggleActive}\n style={{\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n background: isActive\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.9)\n : hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.9),\n color: \"#ffffff\",\n border: \"none\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${hexToRgbaString(\n isActive ? KOREAN_COLORS.ACCENT_RED : KOREAN_COLORS.ACCENT_GREEN,\n 0.8\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n >\n {isActive ? \"훈련 중지 | Stop Drill\" : \"훈련 시작 | Start Drill\"}\n </button>\n </div>\n );\n},\n(prevProps, nextProps) => {\n // Re-render when drill state, mobile state, or callbacks change\n // Including callback props prevents stale closures when parent provides\n // new functions that capture updated state.\n return (\n prevProps.currentDrill === nextProps.currentDrill &&\n prevProps.currentStep === nextProps.currentStep &&\n prevProps.isActive === nextProps.isActive &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onDrillChange === nextProps.onDrillChange &&\n prevProps.onToggleActive === nextProps.onToggleActive\n );\n});\n\nFootworkDrillsOverlayHtml.displayName = \"FootworkDrillsOverlayHtml\";\n\nexport default FootworkDrillsOverlayHtml;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FootworkDrillsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/FootworkDrillsOverlayHtml.tsx"],"sourcesContent":["/**\n * FootworkDrillsOverlayHtml - Training component for footwork drills\n * \n * Provides specialized footwork training exercises for Korean martial arts\n * footwork patterns (보법, Bobeop).\n * \n * @module components/screens/training/components/FootworkDrillsOverlayHtml\n * @category Training Components\n * @korean 보법훈련컴포넌트\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Footwork drill types for training\n * \n * @korean 보법훈련타입\n */\nexport type FootworkDrill = \n | \"circular_left\" // 원형보 좌 - Circle left around target\n | \"circular_right\" // 원형보 우 - Circle right around target\n | \"pivot_combo\" // 축족회전 - Pivot left-right combo\n | \"triangle_step\" // 삼각보법 - Triangle stepping pattern\n | \"slide_drill\" // 미끄럼보 - Four-direction slide drill\n | \"shuffle_practice\" // 섞음보 - Quick shuffle adjustments\n | \"free_practice\"; // 자유 연습 - Free practice mode\n\n/**\n * Drill information with Korean terminology\n * \n * @korean 훈련정보\n */\nconst DRILL_INFO: Record<FootworkDrill, { \n korean: string; \n english: string; \n description: string;\n pattern: string[];\n keyHints: string;\n}> = {\n circular_left: {\n korean: \"원형보 좌회전\",\n english: \"Circular Left\",\n description: \"원형보 좌측 | Circle stepping left\",\n pattern: [\"Ctrl+A\", \"Ctrl+A\", \"Ctrl+A\", \"Ctrl+A\"],\n keyHints: \"Hold Ctrl+A to circle left\",\n },\n circular_right: {\n korean: \"원형보 우회전\",\n english: \"Circular Right\",\n description: \"원형보 우측 | Circle stepping right\",\n pattern: [\"Ctrl+D\", \"Ctrl+D\", \"Ctrl+D\", \"Ctrl+D\"],\n keyHints: \"Hold Ctrl+D to circle right\",\n },\n pivot_combo: {\n korean: \"축족회전 연속\",\n english: \"Pivot Combo\",\n description: \"좌우 연속 회전 | Continuous pivot rotations\",\n pattern: [\"Shift+Ctrl+A\", \"Shift+Ctrl+D\", \"Shift+Ctrl+A\", \"Shift+Ctrl+D\"],\n keyHints: \"Alternate Shift+Ctrl+A/D\",\n },\n triangle_step: {\n korean: \"삼각보법\",\n english: \"Triangle Step\",\n description: \"삼각형 발놀림 | Triangle footwork pattern\",\n pattern: [\"Ctrl+W\", \"Shift+Ctrl+D\", \"Ctrl+S\", \"Shift+Ctrl+A\"],\n keyHints: \"Forward → Pivot → Back → Pivot\",\n },\n slide_drill: {\n korean: \"미끄럼보 사방\",\n english: \"Slide Drill\",\n description: \"사방 미끄럼 | Four-direction slides\",\n pattern: [\"Ctrl+W\", \"Alt+D\", \"Ctrl+S\", \"Alt+A\"],\n keyHints: \"Slide in all four directions\",\n },\n shuffle_practice: {\n korean: \"섞음보 연습\",\n english: \"Shuffle Practice\",\n description: \"빠른 조정 | Quick micro-adjustments\",\n pattern: [\"Shift+Ctrl+W\", \"Shift+Ctrl+W\", \"Shift+Ctrl+W\"],\n keyHints: \"Rapid Shift+Ctrl+W/S\",\n },\n free_practice: {\n korean: \"자유 연습\",\n english: \"Free Practice\",\n description: \"자유 보법 | Free footwork exploration\",\n pattern: [],\n keyHints: \"Use any footwork combination\",\n },\n};\n\n/**\n * Props for FootworkDrillsOverlayHtml component\n */\nexport interface FootworkDrillsOverlayHtmlProps {\n /** Currently selected drill */\n readonly currentDrill: FootworkDrill;\n /** Callback when drill changes */\n readonly onDrillChange: (drill: FootworkDrill) => void;\n /** Current step in drill pattern (0-based) */\n readonly currentStep: number;\n /** Callback when drill step completes (optional, not yet implemented) */\n readonly onStepComplete?: () => void;\n /** Whether drill is currently active */\n readonly isActive: boolean;\n /** Callback to start/stop drill */\n readonly onToggleActive: () => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * FootworkDrillsOverlayHtml Component\n * \n * Provides UI for footwork training drills with step-by-step guidance\n * and Korean martial arts terminology.\n * \n * @korean 보법훈련UI컴포넌트\n */\nexport const FootworkDrillsOverlayHtml = React.memo<FootworkDrillsOverlayHtmlProps>(\n ({\n currentDrill,\n onDrillChange,\n currentStep,\n // onStepComplete, // TODO: Use this for drill pattern validation\n isActive,\n onToggleActive,\n isMobile,\n }) => {\n const drillInfo = DRILL_INFO[currentDrill];\n const [showInstructions, setShowInstructions] = useState(true);\n\n // Auto-hide instructions after 5 seconds when drill is active\n useEffect(() => {\n if (isActive && showInstructions) {\n const timer = setTimeout(() => setShowInstructions(false), 5000);\n return () => clearTimeout(timer);\n }\n }, [isActive, showInstructions]);\n\n const handleDrillSelect = useCallback((drill: FootworkDrill) => {\n onDrillChange(drill);\n setShowInstructions(true);\n }, [onDrillChange]);\n\n const panelWidth = isMobile ? 280 : 340;\n const buttonFontSize = isMobile ? \"10px\" : \"11px\";\n const titleFontSize = isMobile ? \"13px\" : \"15px\";\n\n return (\n <div\n style={{\n width: `${panelWidth}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.5)}`,\n }}\n data-testid=\"footwork-drills-html\"\n >\n {/* Header */}\n <div style={{ marginBottom: \"12px\", textAlign: \"center\" }}>\n <div\n style={{\n fontSize: titleFontSize,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n marginBottom: \"4px\",\n }}\n >\n 보법 훈련 | Footwork Drills\n </div>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n }}\n >\n {drillInfo.korean} | {drillInfo.english}\n </div>\n </div>\n\n {/* Drill Selection Grid */}\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: isMobile ? \"1fr 1fr\" : \"1fr 1fr 1fr\",\n gap: \"6px\",\n marginBottom: \"12px\",\n }}\n >\n {(Object.keys(DRILL_INFO) as FootworkDrill[]).map((drill) => (\n <button\n key={drill}\n onClick={() => handleDrillSelect(drill)}\n style={{\n padding: isMobile ? \"6px 4px\" : \"8px 6px\",\n fontSize: buttonFontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: currentDrill === drill ? \"bold\" : \"normal\",\n background: \n currentDrill === drill\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.8),\n color: \n currentDrill === drill\n ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n border: `1px solid ${hexToRgbaString(\n currentDrill === drill\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.UI_BORDER,\n 0.6\n )}`,\n borderRadius: \"6px\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n lineHeight: 1.2,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n {DRILL_INFO[drill].korean.split(\" \")[0]}\n </button>\n ))}\n </div>\n\n {/* Drill Description */}\n <div\n style={{\n padding: \"8px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.6),\n borderRadius: \"6px\",\n marginBottom: \"10px\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n textAlign: \"center\",\n }}\n >\n {drillInfo.description}\n </div>\n\n {/* Pattern Steps (if drill has pattern) */}\n {drillInfo.pattern.length > 0 && (\n <div style={{ marginBottom: \"10px\" }}>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n marginBottom: \"6px\",\n textAlign: \"center\",\n }}\n >\n Pattern Steps:\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"center\",\n gap: \"4px\",\n flexWrap: \"wrap\",\n }}\n >\n {drillInfo.pattern.map((step, index) => (\n <div\n key={index}\n style={{\n padding: \"4px 8px\",\n fontSize: isMobile ? \"9px\" : \"10px\",\n fontFamily: FONT_FAMILY.KOREAN,\n background: \n isActive && index === currentStep\n ? hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)\n : index < currentStep\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.7),\n color: \n isActive && index === currentStep\n ? hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n border: `1px solid ${hexToRgbaString(\n isActive && index === currentStep\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.UI_BORDER,\n 0.8\n )}`,\n borderRadius: \"4px\",\n fontWeight: isActive && index === currentStep ? \"bold\" : \"normal\",\n }}\n >\n {index + 1}. {step}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Key Hints */}\n {showInstructions && (\n <div\n style={{\n padding: \"8px\",\n background: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n marginBottom: \"10px\",\n fontSize: isMobile ? \"9px\" : \"10px\",\n textAlign: \"center\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n 💡 {drillInfo.keyHints}\n </div>\n )}\n\n {/* Start/Stop Button */}\n <button\n onClick={onToggleActive}\n style={{\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n background: isActive\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.9)\n : hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.9),\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n border: \"none\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${hexToRgbaString(\n isActive ? KOREAN_COLORS.ACCENT_RED : KOREAN_COLORS.ACCENT_GREEN,\n 0.8\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n >\n {isActive ? \"훈련 중지 | Stop Drill\" : \"훈련 시작 | Start Drill\"}\n </button>\n </div>\n );\n},\n(prevProps, nextProps) => {\n // Re-render when drill state, mobile state, or callbacks change\n // Including callback props prevents stale closures when parent provides\n // new functions that capture updated state.\n return (\n prevProps.currentDrill === nextProps.currentDrill &&\n prevProps.currentStep === nextProps.currentStep &&\n prevProps.isActive === nextProps.isActive &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onDrillChange === nextProps.onDrillChange &&\n prevProps.onToggleActive === nextProps.onToggleActive\n );\n});\n\nFootworkDrillsOverlayHtml.displayName = \"FootworkDrillsOverlayHtml\";\n\nexport default FootworkDrillsOverlayHtml;\n"],"mappings":"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"}
|
|
@@ -146,7 +146,7 @@ var TrainingTopHUD = ({ width, isMobile, positionScale, isTraining, onStartTrain
|
|
|
146
146
|
padding: `${SPACING.xxs} ${SPACING.xs}`,
|
|
147
147
|
background: HUD_STYLE.background,
|
|
148
148
|
border: BORDERS.muted,
|
|
149
|
-
borderRadius:
|
|
149
|
+
borderRadius: BORDER_RADIUS.sm,
|
|
150
150
|
fontSize: TYPOGRAPHY.caption.fontSize,
|
|
151
151
|
fontFamily: TYPOGRAPHY.caption.fontFamily,
|
|
152
152
|
color: HIERARCHY.accent.color
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingTopHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingTopHUD.tsx"],"sourcesContent":["/**\n * TrainingTopHUD - Slim top bar for training screen\n *\n * Gaming Best Practice - Minimal Top Bar:\n * - Training Active/Stop indicator (left)\n * - Vital Point hint + Archetype Selector (center) - desktop only\n * - Return to Menu button (right) - Standard gaming pattern\n *\n * Mobile:\n * - Only shows Training status (left) and Return button (right)\n * - Other controls consolidated in BottomHUD\n *\n * Layout:\n * - Width: 100% of screen\n * - Height: Compact 50-70px (minimal obstruction)\n *\n * @korean 훈련화면 상단 바 - 훈련 상태, 급소 힌트, 원형 선택, 메뉴 복귀\n */\n\nimport React from \"react\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { HUD_HEIGHT } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, SPACING_NUMERIC, SPACING_ADJUSTMENTS, BORDER_RADIUS, TYPOGRAPHY, TYPOGRAPHY_NUMERIC, HIERARCHY, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n ArchetypeSelectionButtons,\n ReturnToMenuButton,\n} from \"../TrainingButtonsOverlayHtml\";\nimport TrainingControlsOverlayHtml from \"../TrainingControlsOverlayHtml\";\n\nexport interface TrainingTopHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile layout is active */\n readonly isMobile: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Whether training is currently active */\n readonly isTraining: boolean;\n /** Handler to start training */\n readonly onStartTraining: () => void;\n /** Handler to stop training */\n readonly onStopTraining: () => void;\n /** Currently selected archetype */\n readonly selectedArchetype: PlayerArchetype;\n /** Handler for archetype selection */\n readonly onArchetypeSelect: (archetype: PlayerArchetype) => void;\n /** Whether vital point overlay is visible */\n readonly overlayVisible: boolean;\n /** Handler for returning to menu */\n readonly onReturnToMenu: () => void;\n /** Handler for playing sound effects */\n readonly onPlaySFX: (sound: string) => void;\n}\n\n/**\n * TrainingTopHUD Component\n *\n * Slim top bar containing training controls, vital point hint, archetype selector,\n * and return to menu button. On mobile, only essential controls shown.\n */\nexport const TrainingTopHUD: React.FC<TrainingTopHUDProps> = ({\n width,\n isMobile,\n positionScale,\n isTraining,\n onStartTraining,\n onStopTraining,\n selectedArchetype,\n onArchetypeSelect,\n overlayVisible,\n onReturnToMenu,\n onPlaySFX,\n}) => {\n // Layout calculations for slim top bar\n const layout = React.useMemo(() => {\n const hudHeight = isMobile\n ? HUD_HEIGHT.TRAINING_TOP_MOBILE\n : HUD_HEIGHT.TRAINING_TOP_DESKTOP * positionScale;\n\n const padding = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const gap = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const fontSize = isMobile ? TYPOGRAPHY_NUMERIC.bodySmall : TYPOGRAPHY_NUMERIC.bodySmall * positionScale;\n\n return {\n hudHeight,\n padding,\n gap,\n fontSize,\n hudWidth: width,\n };\n }, [width, isMobile, positionScale]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: `${layout.padding}px ${SPACING_ADJUSTMENTS.horizontalEmphasis}`,\n pointerEvents: \"none\",\n boxSizing: \"border-box\",\n borderBottom: BORDERS.default,\n background: GRADIENTS.vertical(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-top-hud\"\n >\n {/* Left Section - Training Controls (compact) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n gap: `${layout.gap}px`,\n pointerEvents: \"all\",\n alignItems: \"center\",\n }}\n data-testid=\"training-top-hud-left-section\"\n >\n <TrainingControlsOverlayHtml\n isTraining={isTraining}\n onStartTraining={onStartTraining}\n onStopTraining={onStopTraining}\n isMobile={isMobile}\n />\n </div>\n\n {/* Center Section - Vital Point Hint + Archetype Selector (desktop) */}\n {!isMobile && (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap * 2}px`,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n {/* Vital Point Hint */}\n {!overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING_ADJUSTMENTS.xsPlus}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: `${layout.fontSize}px`,\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n color: HIERARCHY.accent.color,\n whiteSpace: \"nowrap\",\n }}\n data-testid=\"vital-point-hint\"\n >\n 💡 Press{\" \"}\n <span\n style={{\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n for vital points\n </div>\n )}\n\n {/* Archetype Selector */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap}px`,\n padding: `${SPACING_ADJUSTMENTS.compact} ${SPACING.sm}`,\n background: HUD_STYLE.background,\n border: BORDERS.accent,\n borderRadius: BORDER_RADIUS.md,\n pointerEvents: \"all\",\n }}\n >\n <span\n style={{\n fontSize: `${layout.fontSize}px`,\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n whiteSpace: \"nowrap\",\n }}\n >\n 원형 | Archetype:\n </span>\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX}\n isMobile={isMobile}\n />\n </div>\n </div>\n )}\n\n {/* Mobile Center - Just vital point hint */}\n {isMobile && !overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: SPACING.xxs,\n fontSize: TYPOGRAPHY.caption.fontSize,\n fontFamily: TYPOGRAPHY.caption.fontFamily,\n color: HIERARCHY.accent.color,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n <span style={{ color: HIERARCHY.gold.color }}>\n V\n </span>{\" \"}\n = 급소\n </div>\n )}\n\n {/* Right Section - Return to Menu */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n pointerEvents: \"all\",\n }}\n data-testid=\"training-top-hud-right-section\"\n >\n <ReturnToMenuButton\n onClick={onReturnToMenu}\n onMouseEnter={() => onPlaySFX(\"menu_hover\")}\n isMobile={isMobile}\n />\n </div>\n </div>\n );\n};\n\nexport default TrainingTopHUD;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,IAAa,kBAAiD,EAC5D,OACA,UACA,eACA,YACA,iBACA,gBACA,mBACA,mBACA,gBACA,gBACA,gBACI;CAEJ,MAAM,SAAS,MAAM,cAAc;AASjC,SAAO;GACL,WATgB,WACd,WAAW,sBACX,WAAW,uBAAuB;GAQpC,SANc,WAAW,gBAAgB,KAAK,gBAAgB,KAAK;GAOnE,KANU,WAAW,gBAAgB,KAAK,gBAAgB,KAAK;GAO/D,UANe,WAAW,mBAAmB,YAAY,mBAAmB,YAAY;GAOxF,UAAU;GACX;IACA;EAAC;EAAO;EAAU;EAAc,CAAC;AAEpC,QACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ,GAAG,OAAO,UAAU;GAC5B,SAAS;GACT,eAAe;GACf,gBAAgB;GAChB,YAAY;GACZ,SAAS,GAAG,OAAO,QAAQ,KAAK,oBAAoB;GACpD,eAAe;GACf,WAAW;GACX,cAAc,QAAQ;GACtB,YAAY,UAAU,SAAS,GAAI;GACnC,gBAAgB,UAAU;GAC3B;EACD,eAAY;YAlBd;GAqBE,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,eAAe;KACf,KAAK,GAAG,OAAO,IAAI;KACnB,eAAe;KACf,YAAY;KACb;IACD,eAAY;cAEZ,oBAAC,6BAAD;KACc;KACK;KACD;KACN;KACV,CAAA;IACE,CAAA;GAGL,CAAC,YACA,qBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,eAAe;KACf,YAAY;KACZ,KAAK,GAAG,OAAO,MAAM,EAAE;KACxB;IACD,eAAY;cAPd,CAUG,CAAC,kBACA,qBAAC,OAAD;KACE,OAAO;MACL,SAAS,GAAG,QAAQ,IAAI,GAAG,oBAAoB;MAC/C,YAAY,UAAU;MACtB,QAAQ,QAAQ;MAChB,cAAc,cAAc;MAC5B,UAAU,GAAG,OAAO,SAAS;MAC7B,YAAY,WAAW,UAAU;MACjC,OAAO,UAAU,OAAO;MACxB,YAAY;MACb;KACD,eAAY;eAXd;MAYC;MACU;MACT,oBAAC,QAAD;OACE,OAAO;QACL,OAAO,UAAU,KAAK;QACtB,YAAY;QACb;iBACF;OAEM,CAAA;MAAC;MAAI;MAER;QAIR,qBAAC,OAAD;KACE,OAAO;MACL,SAAS;MACT,eAAe;MACf,YAAY;MACZ,KAAK,GAAG,OAAO,IAAI;MACnB,SAAS,GAAG,oBAAoB,QAAQ,GAAG,QAAQ;MACnD,YAAY,UAAU;MACtB,QAAQ,QAAQ;MAChB,cAAc,cAAc;MAC5B,eAAe;MAChB;eAXH,CAaE,oBAAC,QAAD;MACE,OAAO;OACL,UAAU,GAAG,OAAO,SAAS;OAC7B,OAAO,UAAU,KAAK;OACtB,YAAY;OACZ,YAAY,WAAW,UAAU;OACjC,YAAY;OACb;gBACF;MAEM,CAAA,EACP,oBAAC,2BAAD;MACqB;MACA;MACR;MACD;MACV,CAAA,CACE;OACF;;GAIP,YAAY,CAAC,kBACZ,qBAAC,OAAD;IACE,OAAO;KACL,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;KACnC,YAAY,UAAU;KACtB,QAAQ,QAAQ;KAChB,cAAc,QAAQ;KACtB,UAAU,WAAW,QAAQ;KAC7B,YAAY,WAAW,QAAQ;KAC/B,OAAO,UAAU,OAAO;KACzB;IACD,eAAY;cAVd;KAYE,oBAAC,QAAD;MAAM,OAAO,EAAE,OAAO,UAAU,KAAK,OAAO;gBAAE;MAEvC,CAAA;KAAC;KAAI;KAER;;GAIR,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,eAAe;KACf,YAAY;KACZ,eAAe;KAChB;IACD,eAAY;cAEZ,oBAAC,oBAAD;KACE,SAAS;KACT,oBAAoB,UAAU,aAAa;KACjC;KACV,CAAA;IACE,CAAA;GACF"}
|
|
1
|
+
{"version":3,"file":"TrainingTopHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingTopHUD.tsx"],"sourcesContent":["/**\n * TrainingTopHUD - Slim top bar for training screen\n *\n * Gaming Best Practice - Minimal Top Bar:\n * - Training Active/Stop indicator (left)\n * - Vital Point hint + Archetype Selector (center) - desktop only\n * - Return to Menu button (right) - Standard gaming pattern\n *\n * Mobile:\n * - Only shows Training status (left) and Return button (right)\n * - Other controls consolidated in BottomHUD\n *\n * Layout:\n * - Width: 100% of screen\n * - Height: Compact 50-70px (minimal obstruction)\n *\n * @korean 훈련화면 상단 바 - 훈련 상태, 급소 힌트, 원형 선택, 메뉴 복귀\n */\n\nimport React from \"react\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { HUD_HEIGHT } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, SPACING_NUMERIC, SPACING_ADJUSTMENTS, BORDER_RADIUS, TYPOGRAPHY, TYPOGRAPHY_NUMERIC, HIERARCHY, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n ArchetypeSelectionButtons,\n ReturnToMenuButton,\n} from \"../TrainingButtonsOverlayHtml\";\nimport TrainingControlsOverlayHtml from \"../TrainingControlsOverlayHtml\";\n\nexport interface TrainingTopHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile layout is active */\n readonly isMobile: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Whether training is currently active */\n readonly isTraining: boolean;\n /** Handler to start training */\n readonly onStartTraining: () => void;\n /** Handler to stop training */\n readonly onStopTraining: () => void;\n /** Currently selected archetype */\n readonly selectedArchetype: PlayerArchetype;\n /** Handler for archetype selection */\n readonly onArchetypeSelect: (archetype: PlayerArchetype) => void;\n /** Whether vital point overlay is visible */\n readonly overlayVisible: boolean;\n /** Handler for returning to menu */\n readonly onReturnToMenu: () => void;\n /** Handler for playing sound effects */\n readonly onPlaySFX: (sound: string) => void;\n}\n\n/**\n * TrainingTopHUD Component\n *\n * Slim top bar containing training controls, vital point hint, archetype selector,\n * and return to menu button. On mobile, only essential controls shown.\n */\nexport const TrainingTopHUD: React.FC<TrainingTopHUDProps> = ({\n width,\n isMobile,\n positionScale,\n isTraining,\n onStartTraining,\n onStopTraining,\n selectedArchetype,\n onArchetypeSelect,\n overlayVisible,\n onReturnToMenu,\n onPlaySFX,\n}) => {\n // Layout calculations for slim top bar\n const layout = React.useMemo(() => {\n const hudHeight = isMobile\n ? HUD_HEIGHT.TRAINING_TOP_MOBILE\n : HUD_HEIGHT.TRAINING_TOP_DESKTOP * positionScale;\n\n const padding = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const gap = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const fontSize = isMobile ? TYPOGRAPHY_NUMERIC.bodySmall : TYPOGRAPHY_NUMERIC.bodySmall * positionScale;\n\n return {\n hudHeight,\n padding,\n gap,\n fontSize,\n hudWidth: width,\n };\n }, [width, isMobile, positionScale]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: `${layout.padding}px ${SPACING_ADJUSTMENTS.horizontalEmphasis}`,\n pointerEvents: \"none\",\n boxSizing: \"border-box\",\n borderBottom: BORDERS.default,\n background: GRADIENTS.vertical(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-top-hud\"\n >\n {/* Left Section - Training Controls (compact) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n gap: `${layout.gap}px`,\n pointerEvents: \"all\",\n alignItems: \"center\",\n }}\n data-testid=\"training-top-hud-left-section\"\n >\n <TrainingControlsOverlayHtml\n isTraining={isTraining}\n onStartTraining={onStartTraining}\n onStopTraining={onStopTraining}\n isMobile={isMobile}\n />\n </div>\n\n {/* Center Section - Vital Point Hint + Archetype Selector (desktop) */}\n {!isMobile && (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap * 2}px`,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n {/* Vital Point Hint */}\n {!overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING_ADJUSTMENTS.xsPlus}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: `${layout.fontSize}px`,\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n color: HIERARCHY.accent.color,\n whiteSpace: \"nowrap\",\n }}\n data-testid=\"vital-point-hint\"\n >\n 💡 Press{\" \"}\n <span\n style={{\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n for vital points\n </div>\n )}\n\n {/* Archetype Selector */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap}px`,\n padding: `${SPACING_ADJUSTMENTS.compact} ${SPACING.sm}`,\n background: HUD_STYLE.background,\n border: BORDERS.accent,\n borderRadius: BORDER_RADIUS.md,\n pointerEvents: \"all\",\n }}\n >\n <span\n style={{\n fontSize: `${layout.fontSize}px`,\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n whiteSpace: \"nowrap\",\n }}\n >\n 원형 | Archetype:\n </span>\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX}\n isMobile={isMobile}\n />\n </div>\n </div>\n )}\n\n {/* Mobile Center - Just vital point hint */}\n {isMobile && !overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: TYPOGRAPHY.caption.fontSize,\n fontFamily: TYPOGRAPHY.caption.fontFamily,\n color: HIERARCHY.accent.color,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n <span style={{ color: HIERARCHY.gold.color }}>\n V\n </span>{\" \"}\n = 급소\n </div>\n )}\n\n {/* Right Section - Return to Menu */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n pointerEvents: \"all\",\n }}\n data-testid=\"training-top-hud-right-section\"\n >\n <ReturnToMenuButton\n onClick={onReturnToMenu}\n onMouseEnter={() => onPlaySFX(\"menu_hover\")}\n isMobile={isMobile}\n />\n </div>\n </div>\n );\n};\n\nexport default TrainingTopHUD;\n"],"mappings":"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"}
|
|
@@ -72,6 +72,12 @@ export interface ResponsiveContainerProps {
|
|
|
72
72
|
readonly safeAreaEdge?: "top" | "bottom" | "left" | "right";
|
|
73
73
|
/** Data test ID for testing */
|
|
74
74
|
readonly "data-testid"?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Optional component name for development-mode debug warnings.
|
|
77
|
+
* When provided, console warnings include this name for easier debugging.
|
|
78
|
+
* @example "CombatLeftHUD"
|
|
79
|
+
*/
|
|
80
|
+
readonly componentName?: string;
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
77
83
|
* ResponsiveContainer Component
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResponsiveContainer.d.ts","sourceRoot":"","sources":["../../../../src/components/shared/base/ResponsiveContainer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAkB,MAAM,OAAO,CAAC;AAEvC,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACZ,MAAM,4BAA4B,CAAC;AAEpC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAEnC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAE7B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAEvC,iCAAiC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IAE/C;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAE3C,uBAAuB;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,wBAAwB;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAEhC,gDAAgD;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC,iDAAiD;IACjD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,kDAAkD;IAClD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC,iCAAiC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAErC,gDAAgD;IAChD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAE/B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAE5D,+BAA+B;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,wBAAwB,
|
|
1
|
+
{"version":3,"file":"ResponsiveContainer.d.ts","sourceRoot":"","sources":["../../../../src/components/shared/base/ResponsiveContainer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAkB,MAAM,OAAO,CAAC;AAEvC,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACZ,MAAM,4BAA4B,CAAC;AAEpC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAEnC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAE7B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAEvC,iCAAiC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IAE/C;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAE3C,uBAAuB;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,wBAAwB;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAEhC,gDAAgD;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC,iDAAiD;IACjD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,kDAAkD;IAClD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC,iCAAiC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAErC,gDAAgD;IAChD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAE/B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAE5D,+BAA+B;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,wBAAwB,CAgJlE,CAAC;AAEF;;GAEG;AACH,eAAe,mBAAmB,CAAC"}
|
|
@@ -120,10 +120,9 @@ var SingleFeedback = ({ feedback, isMobile, arenaBounds, animationDuration }) =>
|
|
|
120
120
|
* ```
|
|
121
121
|
*/
|
|
122
122
|
var ActionFeedback = ({ feedbacks, isMobile = false, arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS, animationDuration = 1200 }) => {
|
|
123
|
-
const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);
|
|
124
123
|
return /* @__PURE__ */ jsx("group", {
|
|
125
124
|
"data-testid": "action-feedback-container",
|
|
126
|
-
children:
|
|
125
|
+
children: useMemo(() => [...feedbacks], [feedbacks]).map((feedback) => /* @__PURE__ */ jsx(SingleFeedback, {
|
|
127
126
|
feedback,
|
|
128
127
|
isMobile,
|
|
129
128
|
arenaBounds,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n// Animation phase thresholds (as percentage of total duration)\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n // Derive visible feedbacks from props - no need for state sync\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n // Use useState lazy initializer for Date.now() to avoid impure function during render\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n // Animation phases: fade in (0-FADE_IN_THRESHOLD), hold (FADE_IN_THRESHOLD-FADE_OUT_THRESHOLD), fade out (FADE_OUT_THRESHOLD-1)\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n // Fade in phase\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n // Hold phase\n setOpacity(1);\n setScale(1);\n } else {\n // Fade out phase\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n // Position at center of scene, slightly below top\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n// Animation phase thresholds (as percentage of total duration)\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n // Derive visible feedbacks from props - no need for state sync\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n // Use useState lazy initializer for Date.now() to avoid impure function during render\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n // Animation phases: fade in (0-FADE_IN_THRESHOLD), hold (FADE_IN_THRESHOLD-FADE_OUT_THRESHOLD), fade out (FADE_OUT_THRESHOLD-1)\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n // Fade in phase\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n // Hold phase\n setOpacity(1);\n setScale(1);\n } else {\n // Fade out phase\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n // Position at center of scene, slightly below top\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
@@ -120,10 +120,9 @@ SingleDamageNumber.displayName = "SingleDamageNumber";
|
|
|
120
120
|
* ```
|
|
121
121
|
*/
|
|
122
122
|
var DamageNumbersComponent = ({ damages, isMobile = false, arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS, animationDuration = 1500 }) => {
|
|
123
|
-
const visibleDamages = useMemo(() => [...damages], [damages]);
|
|
124
123
|
return /* @__PURE__ */ jsx("group", {
|
|
125
124
|
"data-testid": "damage-numbers-container",
|
|
126
|
-
children:
|
|
125
|
+
children: useMemo(() => [...damages], [damages]).map((damage) => /* @__PURE__ */ jsx(SingleDamageNumber, {
|
|
127
126
|
damage,
|
|
128
127
|
isMobile,
|
|
129
128
|
arenaBounds,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n // Calculate critical bonus based on damage type\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n // Custom comparison: re-render only when props that affect rendering change\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n // Derive visible damages from props - no need for state sync\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n // Compare damages array length and contents\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n // Check if array contents changed (compare IDs)\n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n // Check other props\n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n // Calculate 3D position from meter-based coordinates (physics-first architecture)\n // Position is in meters relative to arena center (0, 0)\n // Player models use meter coordinates directly: position={[playerPos.x, 0, playerPos.y]}\n // So we use meter coordinates directly too for alignment\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n // Clamp position to arena boundaries in meters\n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n // Use clamped meter coordinates directly in 3D space (no remapping)\n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n // Update progress using useFrame\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n // Don't render if expired\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n // Calculate critical bonus based on damage type\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n // Custom comparison: re-render only when props that affect rendering change\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n // Derive visible damages from props - no need for state sync\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n // Compare damages array length and contents\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n // Check if array contents changed (compare IDs)\n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n // Check other props\n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
@@ -63,11 +63,11 @@ var BODY_PART_NAMES = {
|
|
|
63
63
|
* Get health bar color based on health percentage
|
|
64
64
|
*/
|
|
65
65
|
var getHealthColor = (health) => {
|
|
66
|
-
if (health >= 80) return
|
|
67
|
-
if (health >= 60) return
|
|
68
|
-
if (health >= 40) return
|
|
69
|
-
if (health >= 20) return
|
|
70
|
-
return
|
|
66
|
+
if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;
|
|
67
|
+
if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;
|
|
68
|
+
if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;
|
|
69
|
+
if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;
|
|
70
|
+
return KOREAN_COLORS.NEGATIVE_RED_DARK;
|
|
71
71
|
};
|
|
72
72
|
/**
|
|
73
73
|
* BodyPartHealthDisplay - Shows health bars for all 8 body parts
|