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
package/DATA_MODEL.md
CHANGED
|
@@ -23,6 +23,14 @@ Black Trigram (흑괘) is a frontend-only Korean martial arts combat simulator w
|
|
|
23
23
|
- ✅ **Session-Only Storage**: No backend persistence (browser session state)
|
|
24
24
|
- ✅ **Functional Design**: Pure functions and immutable state updates
|
|
25
25
|
|
|
26
|
+
**Data Model Sections:**
|
|
27
|
+
- [Core Type System](#core-type-system) — Entity relationships and base interfaces
|
|
28
|
+
- [TypeScript Interface Examples](#typescript-interface-examples) — Key interface definitions
|
|
29
|
+
- [Advanced Type Definitions](#advanced-type-definitions) — Facial, injury, muscle, and clothing types
|
|
30
|
+
- [Type Organization Guide](#type-organization-guide) — Where to find and add types
|
|
31
|
+
- [Skeletal Animation Data Model](#skeletal-animation-data-model) — 28-bone skeletal hierarchy
|
|
32
|
+
- Cross-reference: [State Diagrams](STATEDIAGRAM.md) for state transitions, [Flowcharts](FLOWCHART.md) for process flows
|
|
33
|
+
|
|
26
34
|
---
|
|
27
35
|
|
|
28
36
|
## 📐 Core Type System
|
|
@@ -333,6 +341,345 @@ function updatePlayerHealth(
|
|
|
333
341
|
|
|
334
342
|
---
|
|
335
343
|
|
|
344
|
+
## 🎨 Advanced Type Definitions
|
|
345
|
+
|
|
346
|
+
The following type definitions cover specialized subsystems for realistic combat visualization. These types are defined in dedicated files under `src/types/` and provide strict TypeScript interfaces for facial animation, injury tracking, muscle tension, and clothing systems.
|
|
347
|
+
|
|
348
|
+
### **Facial Animation System from src/types/facial.ts**
|
|
349
|
+
|
|
350
|
+
The facial animation system provides realistic emotion and damage feedback during combat, with smooth expression transitions and eye tracking.
|
|
351
|
+
|
|
352
|
+
#### `FacialExpression` - Expression States
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// src/types/facial.ts
|
|
356
|
+
export enum FacialExpression {
|
|
357
|
+
NEUTRAL = "neutral", // 평온 - Calm, ready state
|
|
358
|
+
FOCUSED = "focused", // 집중 - Concentrated, ready to attack
|
|
359
|
+
PAINED = "pained", // 고통 - Pain response after hit
|
|
360
|
+
EXHAUSTED = "exhausted", // 지침 - Low stamina, heavy breathing
|
|
361
|
+
VICTORIOUS = "victorious", // 승리 - Brief satisfaction after successful strike
|
|
362
|
+
DEFEATED = "defeated", // 패배 - Knocked out, unconscious
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### `FacialDamageState` - Damage Tracking
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// src/types/facial.ts
|
|
370
|
+
export interface FacialDamageState {
|
|
371
|
+
readonly leftEyeSwelling: number; // 0-1, 0=none, 1=fully swollen
|
|
372
|
+
readonly rightEyeSwelling: number; // 0-1
|
|
373
|
+
readonly mouthBleeding: number; // 0-1, lip bleeding intensity
|
|
374
|
+
readonly noseBleeding: number; // 0-1
|
|
375
|
+
readonly leftCheekBruise: number; // 0-1
|
|
376
|
+
readonly rightCheekBruise: number; // 0-1
|
|
377
|
+
readonly foreheadBruise: number; // 0-1, forehead bruise/cut
|
|
378
|
+
readonly jawBruise: number; // 0-1
|
|
379
|
+
readonly totalFacialDamage: number; // 0-100, cumulative
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### `ExpressionState` - Transition Management
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// src/types/facial.ts
|
|
387
|
+
export interface ExpressionState {
|
|
388
|
+
readonly expression: FacialExpression;
|
|
389
|
+
readonly intensity: number; // 0-1, degree of expression
|
|
390
|
+
readonly transitionTime: number; // Seconds to transition
|
|
391
|
+
readonly previousExpression?: FacialExpression; // For blending
|
|
392
|
+
readonly transitionProgress?: number; // 0-1, transition progress
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### `HeadMovementType` - Combat Reactions
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// src/types/facial.ts
|
|
400
|
+
export enum HeadMovementType {
|
|
401
|
+
RECOIL = "recoil", // Head snaps back when hit
|
|
402
|
+
NOD = "nod", // Slight nod forward during attack
|
|
403
|
+
TILT = "tilt", // Head tilts to avoid strike
|
|
404
|
+
TURN = "turn", // Head turns to track opponent
|
|
405
|
+
SHAKE = "shake", // Head shakes when stunned
|
|
406
|
+
DROP = "drop", // Head drops when defeated
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### `EyeTrackingState` - Opponent Tracking
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// src/types/facial.ts
|
|
414
|
+
export interface EyeTrackingState {
|
|
415
|
+
readonly targetPosition: THREE.Vector3; // Opponent position
|
|
416
|
+
readonly lookDirection: THREE.Vector3; // Current look direction
|
|
417
|
+
readonly pupilOffset: { x: number; y: number }; // -1 to 1 per axis
|
|
418
|
+
readonly trackingSpeed: number; // Smoothing factor
|
|
419
|
+
readonly enabled: boolean; // Whether tracking is active
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### **Injury Tracking System from src/types/injury.ts**
|
|
426
|
+
|
|
427
|
+
The injury system tracks localized trauma for visualization, separating system logic from UI components.
|
|
428
|
+
|
|
429
|
+
#### `InjuryType` - Injury Classification
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// src/types/injury.ts
|
|
433
|
+
export enum InjuryType {
|
|
434
|
+
BRUISE = "bruise", // Blunt force trauma (부상)
|
|
435
|
+
CUT = "cut", // Sharp weapon/strike (베임)
|
|
436
|
+
LACERATION = "laceration", // Deep cut with blood trail (열상)
|
|
437
|
+
FRACTURE = "fracture", // Bone damage indicator (골절)
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### `Injury` - Individual Injury Data
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// src/types/injury.ts
|
|
445
|
+
export interface Injury {
|
|
446
|
+
readonly id: string; // Unique identifier
|
|
447
|
+
readonly region: BodyRegion; // Body region affected
|
|
448
|
+
readonly type: InjuryType; // Type of injury
|
|
449
|
+
readonly position: [number, number, number]; // [x, y, z] relative to character
|
|
450
|
+
readonly severity: number; // 0.0 to 1.0
|
|
451
|
+
readonly hitCount: number; // Hits to same location (progressive bruising)
|
|
452
|
+
readonly timestamp: number; // Creation timestamp
|
|
453
|
+
readonly playerId?: string | number; // Optional player ID
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### **Muscle Tension System from src/types/muscle.ts**
|
|
460
|
+
|
|
461
|
+
The muscle system provides realistic body tension visualization during combat, with 29 anatomically-positioned muscle groups that flex up to +30% during technique execution.
|
|
462
|
+
|
|
463
|
+
#### `MuscleGroupName` - 29 Anatomical Muscle Groups
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// src/types/muscle.ts
|
|
467
|
+
export type MuscleGroupName =
|
|
468
|
+
// Shoulders (2)
|
|
469
|
+
| "SHOULDER_L" | "SHOULDER_R"
|
|
470
|
+
// Arms (6)
|
|
471
|
+
| "BICEP_L" | "BICEP_R" | "TRICEP_L" | "TRICEP_R" | "FOREARM_L" | "FOREARM_R"
|
|
472
|
+
// Torso Front (5)
|
|
473
|
+
| "PECTORALS" | "CORE" | "ABS" | "OBLIQUES" | "LOWER_ABS"
|
|
474
|
+
// Torso Back (6)
|
|
475
|
+
| "LAT_L" | "LAT_R" | "TRAPEZIUS" | "RHOMBOID" | "ERECTOR_SPINAE_L" | "ERECTOR_SPINAE_R"
|
|
476
|
+
// Hips (2)
|
|
477
|
+
| "HIP_FLEXOR_L" | "HIP_FLEXOR_R"
|
|
478
|
+
// Legs (8)
|
|
479
|
+
| "QUAD_L" | "QUAD_R" | "HAMSTRING_L" | "HAMSTRING_R"
|
|
480
|
+
| "CALF_L" | "CALF_R" | "GLUTE_L" | "GLUTE_R";
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
#### `MuscleGroup` - Anatomical Definition
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// src/types/muscle.ts
|
|
487
|
+
export interface MuscleGroup {
|
|
488
|
+
readonly name: MuscleGroupName;
|
|
489
|
+
readonly baseScale: THREE.Vector3; // Scale when relaxed
|
|
490
|
+
readonly maxFlexScale: THREE.Vector3; // Max scale when flexed (+30%)
|
|
491
|
+
readonly position: THREE.Vector3; // Position relative to character
|
|
492
|
+
readonly geometryParams: {
|
|
493
|
+
readonly radius: number;
|
|
494
|
+
readonly length: number;
|
|
495
|
+
readonly capSegments: number;
|
|
496
|
+
readonly radialSegments: number;
|
|
497
|
+
};
|
|
498
|
+
readonly korean: string; // Korean name (한글)
|
|
499
|
+
readonly english: string; // English name
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### `MuscleActivationState` - Real-Time Tension
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// src/types/muscle.ts
|
|
507
|
+
// Note: tension, targetTension, and isShaking are intentionally mutable
|
|
508
|
+
// for 60fps performance (avoiding object allocation per frame)
|
|
509
|
+
export interface MuscleActivationState {
|
|
510
|
+
readonly muscleGroup: MuscleGroupName;
|
|
511
|
+
tension: number; // 0.0 = relaxed, 1.0 = maximum flex
|
|
512
|
+
targetTension: number; // Target for smooth transitions
|
|
513
|
+
isShaking: boolean; // Exhaustion shaking effect
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### `MuscleSystemConfig` - Performance Configuration
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// src/types/muscle.ts
|
|
521
|
+
export interface MuscleSystemConfig {
|
|
522
|
+
readonly maxFrameTime: number; // Frame budget (default: 3ms)
|
|
523
|
+
readonly muscleCount: number; // Groups to render (default: 20)
|
|
524
|
+
readonly useInstancing: boolean; // GPU instancing optimization
|
|
525
|
+
readonly relaxationDelay: number; // Post-technique relaxation (0.3s)
|
|
526
|
+
readonly exhaustionThreshold: number; // Stamina % for exhaustion (20%)
|
|
527
|
+
readonly shakeFrequency: number; // Hz when exhausted (20Hz)
|
|
528
|
+
readonly shakeAmplitude: number; // Radians (0.02)
|
|
529
|
+
readonly activationSpeed: number; // Activation transition speed (5.0)
|
|
530
|
+
readonly relaxationSpeed: number; // Relaxation transition speed (3.0)
|
|
531
|
+
readonly shakingTensionThreshold: number; // Min tension for shaking (0.3)
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### **Clothing System from src/types/clothing.ts**
|
|
538
|
+
|
|
539
|
+
The clothing system provides visual distinction for the five player archetypes with culturally-appropriate garments, materials, and cyberpunk Korean aesthetic.
|
|
540
|
+
|
|
541
|
+
#### `ClothingType` / `ClothingMaterial` / `ClothingFit` - Classification Types
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// src/types/clothing.ts
|
|
545
|
+
export type ClothingType =
|
|
546
|
+
| "torso" | "pants" | "belt" | "boots"
|
|
547
|
+
| "gloves" | "headgear" | "vest" | "accessory";
|
|
548
|
+
|
|
549
|
+
export type ClothingMaterial =
|
|
550
|
+
| "fabric" // Traditional cloth (dobok, hanbok)
|
|
551
|
+
| "leather" // Leather gear
|
|
552
|
+
| "tactical" // Modern tactical fabric
|
|
553
|
+
| "synthetic" // Synthetic materials
|
|
554
|
+
| "armored" // Armored plating
|
|
555
|
+
| "cybernetic"; // Tech-enhanced materials
|
|
556
|
+
|
|
557
|
+
export type ClothingFit = "tight" | "fitted" | "loose" | "oversized";
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### `ClothingItem` - Individual Clothing Configuration
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// src/types/clothing.ts
|
|
564
|
+
export interface ClothingItem {
|
|
565
|
+
readonly id: string;
|
|
566
|
+
readonly nameKorean: string; // 한글이름
|
|
567
|
+
readonly nameEnglish: string; // English name
|
|
568
|
+
readonly type: ClothingType;
|
|
569
|
+
readonly material: ClothingMaterial;
|
|
570
|
+
readonly fit: ClothingFit;
|
|
571
|
+
readonly colorPrimary: number; // Primary color (hex)
|
|
572
|
+
readonly colorSecondary?: number; // Accent color (hex)
|
|
573
|
+
readonly colorEmissive?: number; // Glow effect color
|
|
574
|
+
readonly emissiveIntensity?: number; // 0-1
|
|
575
|
+
readonly metalness?: number; // 0-1
|
|
576
|
+
readonly roughness?: number; // 0-1
|
|
577
|
+
readonly scaleMultiplier?: number; // Body proportion scale
|
|
578
|
+
readonly attachedBones: string[]; // Skeletal attachment points
|
|
579
|
+
readonly castShadow?: boolean;
|
|
580
|
+
readonly receiveShadow?: boolean;
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
#### `ClothingSet` - Complete Archetype Wardrobe
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// src/types/clothing.ts
|
|
588
|
+
export interface ClothingSet {
|
|
589
|
+
readonly archetype: PlayerArchetype;
|
|
590
|
+
readonly nameKorean: string;
|
|
591
|
+
readonly nameEnglish: string;
|
|
592
|
+
readonly descriptionKorean: string;
|
|
593
|
+
readonly descriptionEnglish: string;
|
|
594
|
+
readonly items: readonly ClothingItem[];
|
|
595
|
+
readonly themeColors: {
|
|
596
|
+
readonly primary: number;
|
|
597
|
+
readonly secondary: number;
|
|
598
|
+
readonly accent: number;
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
#### `ClothingLODSettings` - Level-of-Detail Performance
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// src/types/clothing.ts
|
|
607
|
+
export interface ClothingLODSettings {
|
|
608
|
+
readonly enableLOD: boolean;
|
|
609
|
+
readonly distances: readonly [number, number, number]; // [near, medium, far]
|
|
610
|
+
readonly highDetailSegments: number;
|
|
611
|
+
readonly mediumDetailSegments: number;
|
|
612
|
+
readonly lowDetailSegments: number;
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## 📂 Type Organization Guide
|
|
619
|
+
|
|
620
|
+
Black Trigram types are organized across multiple directories based on their domain scope. This guide helps developers locate the correct type file for any given concern.
|
|
621
|
+
|
|
622
|
+
### **Type File Locations**
|
|
623
|
+
|
|
624
|
+
| Directory | Purpose | Key Files |
|
|
625
|
+
| --- | --- | --- |
|
|
626
|
+
| `src/types/` | Base game types and shared interfaces | `common.ts`, `facial.ts`, `injury.ts`, `muscle.ts`, `clothing.ts`, `skeletal.ts` |
|
|
627
|
+
| `src/types/constants/` | Design tokens and UI constants | `colors.ts`, `typography.ts`, `layout.ts`, `designSystem.ts`, `ui.ts`, `animations.ts`, `performance.ts` |
|
|
628
|
+
| `src/systems/player.ts` | Player state, attributes, and match statistics | `PlayerState`, `PlayerMatchStats`, `BodyPartHealth` |
|
|
629
|
+
| `src/systems/types.ts` | Cross-system shared types | `StatusEffect`, `HitEffect`, `PlayerArchetypeData`, `EffectIntensity` |
|
|
630
|
+
| `src/systems/combat/types.ts` | Combat result and game state | `CombatResult`, `RoundResult`, `MatchStatistics`, `GameState` |
|
|
631
|
+
| `src/systems/bodypart/types.ts` | Body part health tracking | `BodyPart` enum, `BodyPartHealth`, `BodyPartMaxHealth` |
|
|
632
|
+
| `src/systems/vitalpoint/types.ts` | Vital point targeting and techniques | `KoreanTechnique`, `VitalPointHitResult`, `DamageResult` |
|
|
633
|
+
| `src/systems/ai/types.ts` | AI combat decisions | `AIActionType`, `AIDecision`, `VulnerabilityContext`, `CombatContext` |
|
|
634
|
+
| `src/systems/trigram/types.ts` | Stance laterality and parsing | `StanceLaterality`, `StanceWithSide`, `parseStanceWithSide()` |
|
|
635
|
+
|
|
636
|
+
### **Type Hierarchy**
|
|
637
|
+
|
|
638
|
+
```mermaid
|
|
639
|
+
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#2979FF','primaryTextColor':'#fff','primaryBorderColor':'#0D47A1','lineColor':'#00C853','secondaryColor':'#FFD600','tertiaryColor':'#FF3D00'}}}%%
|
|
640
|
+
flowchart TD
|
|
641
|
+
Base[src/types/common.ts<br/>Base Enums & Interfaces<br/>PlayerArchetype, TrigramStance,<br/>CombatState, KoreanText] --> Player[src/systems/player.ts<br/>PlayerState, PlayerMatchStats]
|
|
642
|
+
Base --> Combat[src/systems/combat/types.ts<br/>CombatResult, GameState]
|
|
643
|
+
Base --> VP[src/systems/vitalpoint/types.ts<br/>KoreanTechnique, VitalPointHitResult]
|
|
644
|
+
Base --> Shared[src/systems/types.ts<br/>StatusEffect, HitEffect]
|
|
645
|
+
|
|
646
|
+
Shared --> Combat
|
|
647
|
+
Shared --> VP
|
|
648
|
+
Player --> Combat
|
|
649
|
+
|
|
650
|
+
Base --> Facial[src/types/facial.ts<br/>FacialExpression, FacialDamageState]
|
|
651
|
+
Base --> Injury[src/types/injury.ts<br/>InjuryType, Injury]
|
|
652
|
+
Base --> Muscle[src/types/muscle.ts<br/>MuscleGroupName, MuscleGroup]
|
|
653
|
+
Base --> Clothing[src/types/clothing.ts<br/>ClothingItem, ClothingSet]
|
|
654
|
+
Base --> Skeletal[src/types/skeletal.ts<br/>Bone, SkeletonConfig]
|
|
655
|
+
|
|
656
|
+
VP --> AI[src/systems/ai/types.ts<br/>AIDecision, VulnerabilityContext]
|
|
657
|
+
Base --> BodyPart[src/systems/bodypart/types.ts<br/>BodyPart, BodyPartHealth]
|
|
658
|
+
Base --> Trigram[src/systems/trigram/types.ts<br/>StanceLaterality, StanceWithSide]
|
|
659
|
+
|
|
660
|
+
Constants[src/types/constants/<br/>Design Tokens<br/>Colors, Typography, Layout] -.->|UI styling| Base
|
|
661
|
+
|
|
662
|
+
style Base fill:#2979FF,stroke:#0D47A1,color:#fff
|
|
663
|
+
style Player fill:#00C853,stroke:#00796B,color:#fff
|
|
664
|
+
style Combat fill:#FF3D00,stroke:#BF360C,color:#fff
|
|
665
|
+
style VP fill:#FFD600,stroke:#F57F17,color:#000
|
|
666
|
+
style Shared fill:#9C27B0,stroke:#6A1B9A,color:#fff
|
|
667
|
+
style Constants fill:#9E9E9E,stroke:#616161,color:#fff
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### **When to Add New Types**
|
|
671
|
+
|
|
672
|
+
| Type Scope | Add To | Example |
|
|
673
|
+
| --- | --- | --- |
|
|
674
|
+
| Core game enums/interfaces | `src/types/common.ts` | New `CombatState` variant |
|
|
675
|
+
| Visualization-specific types | `src/types/{domain}.ts` | New facial expression |
|
|
676
|
+
| Combat result/game flow types | `src/systems/combat/types.ts` | New round result field |
|
|
677
|
+
| Player state attributes | `src/systems/player.ts` | New player stat |
|
|
678
|
+
| AI behavior types | `src/systems/ai/types.ts` | New AI action type |
|
|
679
|
+
| Design tokens / UI constants | `src/types/constants/` | New color theme |
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
336
683
|
## 🦴 Skeletal Animation Data Model
|
|
337
684
|
|
|
338
685
|
### **28-Bone Skeletal Hierarchy**
|
package/lib/App.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAQA,OAAO,WAAW,CAAC;AA8BnB,iBAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAQA,OAAO,WAAW,CAAC;AA8BnB,iBAAS,GAAG,4CA4hBX;AAED,eAAe,GAAG,CAAC"}
|
package/lib/App2.js
CHANGED
|
@@ -369,12 +369,6 @@ function App() {
|
|
|
369
369
|
className: "app",
|
|
370
370
|
tabIndex: 0,
|
|
371
371
|
ref: containerRef,
|
|
372
|
-
style: {
|
|
373
|
-
outline: "none",
|
|
374
|
-
width: "100vw",
|
|
375
|
-
height: "100vh",
|
|
376
|
-
overflow: "hidden"
|
|
377
|
-
},
|
|
378
372
|
"data-testid": "app-container",
|
|
379
373
|
children: [renderCurrentScreen(), showPerformanceDebug && /* @__PURE__ */ jsx(PerformanceDebugOverlayHtml, {})]
|
|
380
374
|
});
|
package/lib/App2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App2.js","names":[],"sources":["../src/App.tsx"],"sourcesContent":["import {\n lazy,\n Suspense,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport \"./App.css\";\n// Main application component - manages game state and screen navigation\nimport { useAudio } from \"./audio/AudioProvider\";\nimport { CombatScreen3D as CombatScreen } from \"./components/screens/combat/CombatScreen3D\";\nimport { ControlsScreen3D as ControlsScreen } from \"./components/screens/controls/ControlsScreen3D\";\nimport { EndScreen3D } from \"./components/screens/endscreen\";\nimport { IntroScreen3D as IntroScreen } from \"./components/screens/intro/IntroScreen3D\";\nimport { PhilosophyScreen3D as PhilosophyScreen } from \"./components/screens/philosophy/PhilosophyScreen3D\";\nimport { PerformanceDebugOverlayHtml } from \"./components/shared/debug/PerformanceDebugOverlayHtml\";\nimport { ErrorModal } from \"./components/shared/ui/ErrorModal\";\nimport { LoadingState } from \"./components/shared/ui/LoadingState\";\nimport { SplashScreen } from \"./components/shared/ui/SplashScreen\";\nimport { PlayerState } from \"./systems\";\nimport { MatchStatistics } from \"./systems/combat\";\nimport { GameMode, PlayerArchetype } from \"./types/common\";\nimport { clearPlatformCache, detectPlatform } from \"./utils/deviceDetection\";\nimport { createPlayerFromArchetype } from \"./utils/playerUtils\";\n\n// Lazy load heavy screens\nconst TrainingScreen = lazy(() =>\n import(\"./components/screens/training/TrainingScreen3D\").then((m) => ({\n default: m.TrainingScreen3D,\n })),\n);\n\n// 150ms delay to allow WebGL context cleanup between full-screen 3D scene transitions\nconst SCREEN_TRANSITION_DELAY_MS = 150;\n// 100ms delay for lighter menu/UI transitions where WebGL teardown/re-init cost is lower\nconst MENU_TRANSITION_DELAY_MS = 100;\n\nfunction App() {\n const [gameMode, setGameMode] = useState<GameMode | null>(null);\n const [selectedArchetype, setSelectedArchetype] = useState<PlayerArchetype>(\n PlayerArchetype.MUSA,\n );\n const [isGameActive, setIsGameActive] = useState(false);\n const [gameWinner, setGameWinner] = useState<PlayerState | null>(null);\n const [matchStats, setMatchStats] = useState<MatchStatistics | null>(null);\n const [appReady, setAppReady] = useState(false);\n const [showSplash, setShowSplash] = useState(true);\n const [showAudioError, setShowAudioError] = useState(false);\n // Performance debug overlay toggle (P key in dev mode)\n const [showPerformanceDebug, setShowPerformanceDebug] = useState(false);\n // Transition state to allow WebGL cleanup between screens\n const [isTransitioning, setIsTransitioning] = useState(false);\n const pendingModeRef = useRef<{\n mode: GameMode;\n archetype?: PlayerArchetype;\n } | null>(null);\n\n // Combat players state - managed here so updates persist\n const [combatPlayers, setCombatPlayers] = useState<PlayerState[]>([]);\n\n const audio = useAudio();\n\n // Add responsive screen size detection with proper device detection\n // Uses user-agent detection first for high-res mobile devices\n const [screenSize, setScreenSize] = useState(() => {\n const platform = detectPlatform();\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n };\n });\n\n useEffect(() => {\n // Define handlers outside async function for proper cleanup\n const handleGlobalError = (e: ErrorEvent) => {\n console.error(\"Global error:\", e.error);\n };\n\n const handleUnhandledRejection = (e: PromiseRejectionEvent) => {\n console.error(\"Unhandled promise rejection:\", e.reason);\n if (\n e.reason?.message?.includes(\"Failed to load\") ||\n e.reason?.message?.includes(\"no supported source\")\n ) {\n e.preventDefault();\n }\n };\n\n const initializeApp = async () => {\n try {\n window.focus();\n\n window.addEventListener(\"error\", handleGlobalError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n // PHASE 2: Performance optimization initialization\n console.log(\"🔧 Initializing animation performance optimizations...\");\n\n // 1. Prewarm object pools for animation optimization\n // This eliminates GC pressure from ~1,344 allocations per frame\n const { ThreeObjectPools } = await import(\"./utils/threeObjectPool\");\n ThreeObjectPools.prewarmAll();\n const poolStatus = ThreeObjectPools.getStatus();\n console.log(\" ✓ Object pools prewarmed:\", poolStatus);\n\n // 2. Precompute all animations for 90%+ cache hit rate\n const { precomputeAnimation } =\n await import(\"./systems/animation/core/AnimationOptimizations\");\n const { ALL_ANIMATIONS } =\n await import(\"./systems/animation/core/AnimationRegistry\");\n\n let precomputedCount = 0;\n ALL_ANIMATIONS.forEach((animation) => {\n // Precompute at 60fps for smooth playback\n // Use animation.name as the unique identifier\n precomputeAnimation(animation.name, animation, 60);\n precomputedCount++;\n });\n console.log(` ✓ Precomputed ${precomputedCount} animations at 60fps`);\n\n console.log(\n \"✅ Animation optimizations ready (expect <5ms frame time, 90%+ cache hit)\",\n );\n\n setAppReady(true);\n console.log(\"🎯 Black Trigram app initialized\");\n } catch (error) {\n console.error(\"Failed to initialize app:\", error);\n setAppReady(true);\n }\n };\n\n initializeApp();\n\n // Cleanup global event handlers to prevent memory leaks\n return () => {\n window.removeEventListener(\"error\", handleGlobalError);\n window.removeEventListener(\n \"unhandledrejection\",\n handleUnhandledRejection,\n );\n };\n }, []);\n\n // Shared audio initialization logic for splash and retry\n const initializeAudioWithRetry = useCallback(async () => {\n if (!appReady) {\n console.warn(\"App not ready yet, please wait...\");\n return false;\n }\n try {\n await audio.initializeAudio();\n console.log(\"🎵 Audio initialized\");\n return true;\n } catch (error) {\n console.error(\"Failed to initialize audio:\", error);\n return false;\n }\n }, [audio, appReady]);\n\n // Handle splash screen start - initialize audio on user gesture\n const handleSplashStart = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorRetry = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorContinue = useCallback(() => {\n // Continue without sound\n setShowAudioError(false);\n setShowSplash(false);\n console.log(\"Continuing without audio (silent mode)\");\n }, []);\n\n // ✅ SIMPLIFIED: Handle game mode selection directly\n const handleGameStart = useCallback(\n (mode: GameMode, archetype?: PlayerArchetype) => {\n console.log(\"🎮 Starting game mode:\", mode, \"with archetype:\", archetype);\n\n // Store pending mode and start transition to allow WebGL cleanup\n pendingModeRef.current = { mode, archetype };\n setIsTransitioning(true);\n\n // Clear current mode first (unmounts Canvas)\n setGameMode(null);\n setIsGameActive(false);\n\n // After brief delay, mount new screen\n // Increased delay to allow proper WebGL context cleanup\n setTimeout(() => {\n const pending = pendingModeRef.current;\n if (!pending) return;\n\n // ✅ NEW: Handle controls and philosophy as separate modes\n if (\n pending.mode === GameMode.CONTROLS ||\n pending.mode === GameMode.PHILOSOPHY\n ) {\n setGameMode(pending.mode);\n setIsGameActive(false); // These are not game modes, just screens\n } else {\n setGameMode(pending.mode);\n setIsGameActive(true);\n }\n\n setGameWinner(null);\n setMatchStats(null);\n if (pending.archetype) {\n setSelectedArchetype(pending.archetype);\n }\n\n setIsTransitioning(false);\n pendingModeRef.current = null;\n }, SCREEN_TRANSITION_DELAY_MS); // Delay for WebGL cleanup\n },\n [],\n );\n\n const handleGameEnd = useCallback(\n (winner: number) => {\n setIsGameActive(false);\n setGameWinner(createPlayerFromArchetype(selectedArchetype, winner));\n // Reset combat players for next match\n setCombatPlayers([]);\n\n setMatchStats({\n totalDamageDealt: 150,\n totalDamageTaken: 100,\n criticalHits: 3,\n vitalPointHits: 2,\n techniquesUsed: 8,\n perfectStrikes: 1,\n consecutiveWins: 1,\n matchDuration: 120,\n totalMatches: 1,\n maxRounds: 3,\n winner: winner,\n totalRounds: 2,\n currentRound: 2,\n timeRemaining: 0,\n combatEvents: [],\n finalScore: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n roundsWon: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n player1: {\n wins: winner === 0 ? 1 : 0,\n losses: winner === 0 ? 0 : 1,\n hitsTaken: 5,\n hitsLanded: 8,\n totalDamageDealt: winner === 0 ? 150 : 100,\n totalDamageReceived: winner === 0 ? 100 : 150,\n techniques: [\"천둥벽력\", \"유수연타\"],\n perfectStrikes: winner === 0 ? 1 : 0,\n vitalPointHits: winner === 0 ? 2 : 1,\n consecutiveWins: winner === 0 ? 1 : 0,\n matchDuration: 120,\n },\n player2: {\n wins: winner === 1 ? 1 : 0,\n losses: winner === 1 ? 0 : 1,\n hitsTaken: 8,\n hitsLanded: 5,\n totalDamageDealt: winner === 1 ? 150 : 100,\n totalDamageReceived: winner === 1 ? 100 : 150,\n techniques: [\"화염지창\", \"벽력일섬\"],\n perfectStrikes: winner === 1 ? 1 : 0,\n vitalPointHits: winner === 1 ? 2 : 1,\n consecutiveWins: winner === 1 ? 1 : 0,\n matchDuration: 120,\n },\n });\n },\n [selectedArchetype],\n );\n\n const handleReturnToMenu = useCallback(() => {\n // Use same transition logic for return to menu\n setIsTransitioning(true);\n setGameMode(null);\n setIsGameActive(false);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize next combat\n setCombatPlayers([]);\n setTimeout(() => setIsTransitioning(false), MENU_TRANSITION_DELAY_MS);\n }, []);\n\n const handleRematch = useCallback(() => {\n // Restart combat with same settings\n if (!gameMode) {\n console.error(\n \"Cannot rematch: gameMode is not set. This should not happen - EndScreen only renders when gameMode is set.\",\n { gameMode, isGameActive, gameWinner, matchStats }\n );\n return;\n }\n \n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize for rematch\n setCombatPlayers([]);\n \n setTimeout(() => {\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- isGameActive, gameWinner, matchStats only used in error logging, not function logic\n }, [gameMode]);\n\n const handleViewTraining = useCallback(() => {\n // Navigate to training mode\n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n setCombatPlayers([]);\n \n setTimeout(() => {\n setGameMode(GameMode.TRAINING);\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n }, []);\n\n const renderCurrentScreen = () => {\n // Show loading during screen transitions\n if (isTransitioning) {\n return (\n <LoadingState\n progress={undefined}\n message=\"전환 중... | Transitioning...\"\n stage=\"assets\"\n />\n );\n }\n\n if (gameWinner && matchStats) {\n // ✅ NEW: Use EndScreen3D component\n return (\n <EndScreen3D\n winner={gameWinner}\n matchStats={matchStats}\n onReturnToMenu={handleReturnToMenu}\n onRematch={handleRematch}\n onViewReplay={handleViewTraining}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ NEW: Handle standalone screens first\n if (gameMode === GameMode.CONTROLS) {\n return (\n <ControlsScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n if (gameMode === GameMode.PHILOSOPHY) {\n return (\n <PhilosophyScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ SIMPLIFIED: Only active game modes use isGameActive\n if (isGameActive && gameMode) {\n switch (gameMode) {\n case GameMode.TRAINING:\n return (\n <Suspense\n fallback={\n <LoadingState\n progress={undefined}\n message=\"훈련장 로딩 중... | Loading Training...\"\n stage=\"assets\"\n />\n }\n >\n <TrainingScreen\n onPlayerUpdate={(updates) => {\n console.log(\"Training player updated:\", updates);\n }}\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n initialArchetype={selectedArchetype}\n />\n </Suspense>\n );\n case GameMode.VERSUS:\n case GameMode.PRACTICE:\n // Initialize players if not already set\n if (combatPlayers.length === 0) {\n const player1 = createPlayerFromArchetype(selectedArchetype, 0);\n const player2 = createPlayerFromArchetype(\n PlayerArchetype.AMSALJA,\n 1,\n );\n // Use setTimeout to defer state update and avoid render-during-render\n setTimeout(() => setCombatPlayers([player1, player2]), 0);\n // Return loading state while players initialize\n return (\n <LoadingState\n progress={undefined}\n message=\"전투 준비 중... | Preparing Combat...\"\n stage=\"assets\"\n />\n );\n }\n\n return (\n <CombatScreen\n players={combatPlayers}\n currentRound={1}\n timeRemaining={180}\n isPaused={false}\n onPlayerUpdate={(playerIndex, updates) => {\n // Actually update the player state so damage persists!\n setCombatPlayers((prevPlayers) => {\n const newPlayers = [...prevPlayers];\n if (newPlayers[playerIndex]) {\n newPlayers[playerIndex] = {\n ...newPlayers[playerIndex],\n ...updates,\n };\n }\n return newPlayers;\n });\n }}\n onReturnToMenu={handleReturnToMenu}\n onGameEnd={handleGameEnd}\n gameMode={gameMode}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n default:\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n }\n\n // ✅ SIMPLIFIED: Default to intro screen\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n };\n\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n containerRef.current?.focus();\n }, [appReady]);\n\n useEffect(() => {\n const handleResize = () => {\n // Clear cached platform info to get fresh detection on resize\n clearPlatformCache();\n const platform = detectPlatform();\n setScreenSize({\n width: platform.screenWidth,\n height: platform.screenHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n });\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // F9 key toggle for performance debug overlay (dev mode only)\n // Note: P key is reserved for Philosophy screen\n useEffect(() => {\n if (!import.meta.env.DEV) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"F9\") {\n e.preventDefault();\n setShowPerformanceDebug((prev) => !prev);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, []);\n\n if (!appReady) {\n return (\n <div className=\"app loading\" data-testid=\"app-container\">\n <LoadingState\n progress={undefined}\n message=\"앱 초기화 중 | Initializing app...\"\n stage=\"initialization\"\n />\n </div>\n );\n }\n\n // Show splash screen first to get user gesture for audio\n if (showSplash) {\n return (\n <div className=\"app\" data-testid=\"app-container\">\n <SplashScreen\n onStart={handleSplashStart}\n width={screenSize.width}\n height={screenSize.height}\n />\n {showAudioError && (\n <ErrorModal\n message=\"오디오 초기화에 실패했습니다. 재시도하거나 소리 없이 계속할 수 있습니다. | Audio initialization failed. You can retry or continue without sound.\"\n onRetry={handleAudioErrorRetry}\n onContinue={handleAudioErrorContinue}\n />\n )}\n </div>\n );\n }\n\n return (\n <div\n className=\"app\"\n tabIndex={0}\n ref={containerRef}\n style={{\n outline: \"none\",\n width: \"100vw\",\n height: \"100vh\",\n overflow: \"hidden\",\n }}\n data-testid=\"app-container\"\n >\n {/* All screens now use Three.js or pure React/HTML */}\n {renderCurrentScreen()}\n\n {/* Performance debug overlay (dev mode only, toggle with P key) */}\n {showPerformanceDebug && <PerformanceDebugOverlayHtml />}\n </div>\n );\n}\n\nexport default App;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"App2.js","names":[],"sources":["../src/App.tsx"],"sourcesContent":["import {\n lazy,\n Suspense,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport \"./App.css\";\n// Main application component - manages game state and screen navigation\nimport { useAudio } from \"./audio/AudioProvider\";\nimport { CombatScreen3D as CombatScreen } from \"./components/screens/combat/CombatScreen3D\";\nimport { ControlsScreen3D as ControlsScreen } from \"./components/screens/controls/ControlsScreen3D\";\nimport { EndScreen3D } from \"./components/screens/endscreen\";\nimport { IntroScreen3D as IntroScreen } from \"./components/screens/intro/IntroScreen3D\";\nimport { PhilosophyScreen3D as PhilosophyScreen } from \"./components/screens/philosophy/PhilosophyScreen3D\";\nimport { PerformanceDebugOverlayHtml } from \"./components/shared/debug/PerformanceDebugOverlayHtml\";\nimport { ErrorModal } from \"./components/shared/ui/ErrorModal\";\nimport { LoadingState } from \"./components/shared/ui/LoadingState\";\nimport { SplashScreen } from \"./components/shared/ui/SplashScreen\";\nimport { PlayerState } from \"./systems\";\nimport { MatchStatistics } from \"./systems/combat\";\nimport { GameMode, PlayerArchetype } from \"./types/common\";\nimport { clearPlatformCache, detectPlatform } from \"./utils/deviceDetection\";\nimport { createPlayerFromArchetype } from \"./utils/playerUtils\";\n\n// Lazy load heavy screens\nconst TrainingScreen = lazy(() =>\n import(\"./components/screens/training/TrainingScreen3D\").then((m) => ({\n default: m.TrainingScreen3D,\n })),\n);\n\n// 150ms delay to allow WebGL context cleanup between full-screen 3D scene transitions\nconst SCREEN_TRANSITION_DELAY_MS = 150;\n// 100ms delay for lighter menu/UI transitions where WebGL teardown/re-init cost is lower\nconst MENU_TRANSITION_DELAY_MS = 100;\n\nfunction App() {\n const [gameMode, setGameMode] = useState<GameMode | null>(null);\n const [selectedArchetype, setSelectedArchetype] = useState<PlayerArchetype>(\n PlayerArchetype.MUSA,\n );\n const [isGameActive, setIsGameActive] = useState(false);\n const [gameWinner, setGameWinner] = useState<PlayerState | null>(null);\n const [matchStats, setMatchStats] = useState<MatchStatistics | null>(null);\n const [appReady, setAppReady] = useState(false);\n const [showSplash, setShowSplash] = useState(true);\n const [showAudioError, setShowAudioError] = useState(false);\n // Performance debug overlay toggle (P key in dev mode)\n const [showPerformanceDebug, setShowPerformanceDebug] = useState(false);\n // Transition state to allow WebGL cleanup between screens\n const [isTransitioning, setIsTransitioning] = useState(false);\n const pendingModeRef = useRef<{\n mode: GameMode;\n archetype?: PlayerArchetype;\n } | null>(null);\n\n // Combat players state - managed here so updates persist\n const [combatPlayers, setCombatPlayers] = useState<PlayerState[]>([]);\n\n const audio = useAudio();\n\n // Add responsive screen size detection with proper device detection\n // Uses user-agent detection first for high-res mobile devices\n const [screenSize, setScreenSize] = useState(() => {\n const platform = detectPlatform();\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n };\n });\n\n useEffect(() => {\n // Define handlers outside async function for proper cleanup\n const handleGlobalError = (e: ErrorEvent) => {\n console.error(\"Global error:\", e.error);\n };\n\n const handleUnhandledRejection = (e: PromiseRejectionEvent) => {\n console.error(\"Unhandled promise rejection:\", e.reason);\n if (\n e.reason?.message?.includes(\"Failed to load\") ||\n e.reason?.message?.includes(\"no supported source\")\n ) {\n e.preventDefault();\n }\n };\n\n const initializeApp = async () => {\n try {\n window.focus();\n\n window.addEventListener(\"error\", handleGlobalError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n // PHASE 2: Performance optimization initialization\n console.log(\"🔧 Initializing animation performance optimizations...\");\n\n // 1. Prewarm object pools for animation optimization\n // This eliminates GC pressure from ~1,344 allocations per frame\n const { ThreeObjectPools } = await import(\"./utils/threeObjectPool\");\n ThreeObjectPools.prewarmAll();\n const poolStatus = ThreeObjectPools.getStatus();\n console.log(\" ✓ Object pools prewarmed:\", poolStatus);\n\n // 2. Precompute all animations for 90%+ cache hit rate\n const { precomputeAnimation } =\n await import(\"./systems/animation/core/AnimationOptimizations\");\n const { ALL_ANIMATIONS } =\n await import(\"./systems/animation/core/AnimationRegistry\");\n\n let precomputedCount = 0;\n ALL_ANIMATIONS.forEach((animation) => {\n // Precompute at 60fps for smooth playback\n // Use animation.name as the unique identifier\n precomputeAnimation(animation.name, animation, 60);\n precomputedCount++;\n });\n console.log(` ✓ Precomputed ${precomputedCount} animations at 60fps`);\n\n console.log(\n \"✅ Animation optimizations ready (expect <5ms frame time, 90%+ cache hit)\",\n );\n\n setAppReady(true);\n console.log(\"🎯 Black Trigram app initialized\");\n } catch (error) {\n console.error(\"Failed to initialize app:\", error);\n setAppReady(true);\n }\n };\n\n initializeApp();\n\n // Cleanup global event handlers to prevent memory leaks\n return () => {\n window.removeEventListener(\"error\", handleGlobalError);\n window.removeEventListener(\n \"unhandledrejection\",\n handleUnhandledRejection,\n );\n };\n }, []);\n\n // Shared audio initialization logic for splash and retry\n const initializeAudioWithRetry = useCallback(async () => {\n if (!appReady) {\n console.warn(\"App not ready yet, please wait...\");\n return false;\n }\n try {\n await audio.initializeAudio();\n console.log(\"🎵 Audio initialized\");\n return true;\n } catch (error) {\n console.error(\"Failed to initialize audio:\", error);\n return false;\n }\n }, [audio, appReady]);\n\n // Handle splash screen start - initialize audio on user gesture\n const handleSplashStart = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorRetry = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorContinue = useCallback(() => {\n // Continue without sound\n setShowAudioError(false);\n setShowSplash(false);\n console.log(\"Continuing without audio (silent mode)\");\n }, []);\n\n // ✅ SIMPLIFIED: Handle game mode selection directly\n const handleGameStart = useCallback(\n (mode: GameMode, archetype?: PlayerArchetype) => {\n console.log(\"🎮 Starting game mode:\", mode, \"with archetype:\", archetype);\n\n // Store pending mode and start transition to allow WebGL cleanup\n pendingModeRef.current = { mode, archetype };\n setIsTransitioning(true);\n\n // Clear current mode first (unmounts Canvas)\n setGameMode(null);\n setIsGameActive(false);\n\n // After brief delay, mount new screen\n // Increased delay to allow proper WebGL context cleanup\n setTimeout(() => {\n const pending = pendingModeRef.current;\n if (!pending) return;\n\n // ✅ NEW: Handle controls and philosophy as separate modes\n if (\n pending.mode === GameMode.CONTROLS ||\n pending.mode === GameMode.PHILOSOPHY\n ) {\n setGameMode(pending.mode);\n setIsGameActive(false); // These are not game modes, just screens\n } else {\n setGameMode(pending.mode);\n setIsGameActive(true);\n }\n\n setGameWinner(null);\n setMatchStats(null);\n if (pending.archetype) {\n setSelectedArchetype(pending.archetype);\n }\n\n setIsTransitioning(false);\n pendingModeRef.current = null;\n }, SCREEN_TRANSITION_DELAY_MS); // Delay for WebGL cleanup\n },\n [],\n );\n\n const handleGameEnd = useCallback(\n (winner: number) => {\n setIsGameActive(false);\n setGameWinner(createPlayerFromArchetype(selectedArchetype, winner));\n // Reset combat players for next match\n setCombatPlayers([]);\n\n setMatchStats({\n totalDamageDealt: 150,\n totalDamageTaken: 100,\n criticalHits: 3,\n vitalPointHits: 2,\n techniquesUsed: 8,\n perfectStrikes: 1,\n consecutiveWins: 1,\n matchDuration: 120,\n totalMatches: 1,\n maxRounds: 3,\n winner: winner,\n totalRounds: 2,\n currentRound: 2,\n timeRemaining: 0,\n combatEvents: [],\n finalScore: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n roundsWon: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n player1: {\n wins: winner === 0 ? 1 : 0,\n losses: winner === 0 ? 0 : 1,\n hitsTaken: 5,\n hitsLanded: 8,\n totalDamageDealt: winner === 0 ? 150 : 100,\n totalDamageReceived: winner === 0 ? 100 : 150,\n techniques: [\"천둥벽력\", \"유수연타\"],\n perfectStrikes: winner === 0 ? 1 : 0,\n vitalPointHits: winner === 0 ? 2 : 1,\n consecutiveWins: winner === 0 ? 1 : 0,\n matchDuration: 120,\n },\n player2: {\n wins: winner === 1 ? 1 : 0,\n losses: winner === 1 ? 0 : 1,\n hitsTaken: 8,\n hitsLanded: 5,\n totalDamageDealt: winner === 1 ? 150 : 100,\n totalDamageReceived: winner === 1 ? 100 : 150,\n techniques: [\"화염지창\", \"벽력일섬\"],\n perfectStrikes: winner === 1 ? 1 : 0,\n vitalPointHits: winner === 1 ? 2 : 1,\n consecutiveWins: winner === 1 ? 1 : 0,\n matchDuration: 120,\n },\n });\n },\n [selectedArchetype],\n );\n\n const handleReturnToMenu = useCallback(() => {\n // Use same transition logic for return to menu\n setIsTransitioning(true);\n setGameMode(null);\n setIsGameActive(false);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize next combat\n setCombatPlayers([]);\n setTimeout(() => setIsTransitioning(false), MENU_TRANSITION_DELAY_MS);\n }, []);\n\n const handleRematch = useCallback(() => {\n // Restart combat with same settings\n if (!gameMode) {\n console.error(\n \"Cannot rematch: gameMode is not set. This should not happen - EndScreen only renders when gameMode is set.\",\n { gameMode, isGameActive, gameWinner, matchStats }\n );\n return;\n }\n \n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize for rematch\n setCombatPlayers([]);\n \n setTimeout(() => {\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- isGameActive, gameWinner, matchStats only used in error logging, not function logic\n }, [gameMode]);\n\n const handleViewTraining = useCallback(() => {\n // Navigate to training mode\n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n setCombatPlayers([]);\n \n setTimeout(() => {\n setGameMode(GameMode.TRAINING);\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n }, []);\n\n const renderCurrentScreen = () => {\n // Show loading during screen transitions\n if (isTransitioning) {\n return (\n <LoadingState\n progress={undefined}\n message=\"전환 중... | Transitioning...\"\n stage=\"assets\"\n />\n );\n }\n\n if (gameWinner && matchStats) {\n // ✅ NEW: Use EndScreen3D component\n return (\n <EndScreen3D\n winner={gameWinner}\n matchStats={matchStats}\n onReturnToMenu={handleReturnToMenu}\n onRematch={handleRematch}\n onViewReplay={handleViewTraining}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ NEW: Handle standalone screens first\n if (gameMode === GameMode.CONTROLS) {\n return (\n <ControlsScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n if (gameMode === GameMode.PHILOSOPHY) {\n return (\n <PhilosophyScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ SIMPLIFIED: Only active game modes use isGameActive\n if (isGameActive && gameMode) {\n switch (gameMode) {\n case GameMode.TRAINING:\n return (\n <Suspense\n fallback={\n <LoadingState\n progress={undefined}\n message=\"훈련장 로딩 중... | Loading Training...\"\n stage=\"assets\"\n />\n }\n >\n <TrainingScreen\n onPlayerUpdate={(updates) => {\n console.log(\"Training player updated:\", updates);\n }}\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n initialArchetype={selectedArchetype}\n />\n </Suspense>\n );\n case GameMode.VERSUS:\n case GameMode.PRACTICE:\n // Initialize players if not already set\n if (combatPlayers.length === 0) {\n const player1 = createPlayerFromArchetype(selectedArchetype, 0);\n const player2 = createPlayerFromArchetype(\n PlayerArchetype.AMSALJA,\n 1,\n );\n // Use setTimeout to defer state update and avoid render-during-render\n setTimeout(() => setCombatPlayers([player1, player2]), 0);\n // Return loading state while players initialize\n return (\n <LoadingState\n progress={undefined}\n message=\"전투 준비 중... | Preparing Combat...\"\n stage=\"assets\"\n />\n );\n }\n\n return (\n <CombatScreen\n players={combatPlayers}\n currentRound={1}\n timeRemaining={180}\n isPaused={false}\n onPlayerUpdate={(playerIndex, updates) => {\n // Actually update the player state so damage persists!\n setCombatPlayers((prevPlayers) => {\n const newPlayers = [...prevPlayers];\n if (newPlayers[playerIndex]) {\n newPlayers[playerIndex] = {\n ...newPlayers[playerIndex],\n ...updates,\n };\n }\n return newPlayers;\n });\n }}\n onReturnToMenu={handleReturnToMenu}\n onGameEnd={handleGameEnd}\n gameMode={gameMode}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n default:\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n }\n\n // ✅ SIMPLIFIED: Default to intro screen\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n };\n\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n containerRef.current?.focus();\n }, [appReady]);\n\n useEffect(() => {\n const handleResize = () => {\n // Clear cached platform info to get fresh detection on resize\n clearPlatformCache();\n const platform = detectPlatform();\n setScreenSize({\n width: platform.screenWidth,\n height: platform.screenHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n });\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // F9 key toggle for performance debug overlay (dev mode only)\n // Note: P key is reserved for Philosophy screen\n useEffect(() => {\n if (!import.meta.env.DEV) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"F9\") {\n e.preventDefault();\n setShowPerformanceDebug((prev) => !prev);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, []);\n\n if (!appReady) {\n return (\n <div className=\"app loading\" data-testid=\"app-container\">\n <LoadingState\n progress={undefined}\n message=\"앱 초기화 중 | Initializing app...\"\n stage=\"initialization\"\n />\n </div>\n );\n }\n\n // Show splash screen first to get user gesture for audio\n if (showSplash) {\n return (\n <div className=\"app\" data-testid=\"app-container\">\n <SplashScreen\n onStart={handleSplashStart}\n width={screenSize.width}\n height={screenSize.height}\n />\n {showAudioError && (\n <ErrorModal\n message=\"오디오 초기화에 실패했습니다. 재시도하거나 소리 없이 계속할 수 있습니다. | Audio initialization failed. You can retry or continue without sound.\"\n onRetry={handleAudioErrorRetry}\n onContinue={handleAudioErrorContinue}\n />\n )}\n </div>\n );\n }\n\n return (\n <div\n className=\"app\"\n tabIndex={0}\n ref={containerRef}\n data-testid=\"app-container\"\n >\n {/* All screens now use Three.js or pure React/HTML */}\n {renderCurrentScreen()}\n\n {/* Performance debug overlay (dev mode only, toggle with P key) */}\n {showPerformanceDebug && <PerformanceDebugOverlayHtml />}\n </div>\n );\n}\n\nexport default App;\n"],"mappings":"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"}
|