diff --git a/README.md b/README.md
index 9fe2d5f2cbe..c7f6ff4ac5f 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,19 @@
# ๐ฅ Black Trigram (ํ๊ด)
-### *๋ฌด์๋ ๋ชธ๊ณผ ๋ง์, ๊ทธ๋ฆฌ๊ณ ์ํผ์ ์กฐํ์ด๋ค*
-*"Martial arts are the harmony of body, mind, and spirit"*
-
+### _๋ฌด์๋ ๋ชธ๊ณผ ๋ง์, ๊ทธ๋ฆฌ๊ณ ์ํผ์ ์กฐํ์ด๋ค_
+
+_"Martial arts are the harmony of body, mind, and spirit"_
+
+
[](https://github.com/Hack23/blacktrigram/raw/master/LICENSE.md)
[](https://scorecard.dev/viewer/?uri=github.com/Hack23/blacktrigram)
-*A 2D precision combat game inspired by Korean martial arts philosophy and the I Ching*
+_A 2D precision combat game inspired by Korean martial arts philosophy and the I Ching_
+
+**[๐ฎ Play Now](https://hack23.github.io/blacktrigram/)** | **[๐ Game Guide](#-game-features)** | **[๐ง Development](#-technical-excellence)**
@@ -26,41 +30,41 @@
**๐ฏ ์ ๊ฒฉ์**
-*Jeonggyeokja*
+_Jeonggyeokja_
**Precision Attacker**
-*Every strike calculated
-and deliberate*
+_Every strike calculated
+and deliberate_
**โ๏ธ ๋น์**
-*Bisu*
+_Bisu_
**Lethal Blade**
-*Metaphorical lethality
-in unarmed combat*
+_Metaphorical lethality
+in unarmed combat_
**๐ฅท ์์ด์**
-*Amsalja*
+_Amsalja_
**Assassin**
-*Focus on finishing
-techniques*
+_Focus on finishing
+techniques_
**๐ ๊ธ์๊ฒฉ**
-*Geupsogyeok*
+_Geupsogyeok_
**Vital Point Strike**
-*Core combat
-mechanic*
+_Core combat
+mechanic_
@@ -68,40 +72,42 @@ mechanic*
---
-## ๐ Game Features
+## ๐ Enhanced Game Features
### ๐ฎ Trigram-Based Combat System (ํ๊ด ๋ฌด์ ์ฒด๊ณ)
-Master **8 unique fighting philosophies** based on the I Ching trigrams:
+Master **8 unique fighting philosophies** based on the I Ching trigrams with enhanced visual effects:
-| Trigram | Name | Philosophy | Combat Style |
-|:-------:|:----:|:----------:|:------------:|
-| โฐ | **๊ฑด (Geon)** - *Heaven* | Pure yang energy | ๐ฅ Power strikes, guard breaking |
-| โฑ | **ํ (Tae)** - *Lake* | Joy and flexibility | ๐ Flowing combinations |
-| โฒ | **๋ฆฌ (Li)** - *Fire* | Clarity and intensity | โก Fast, precise attacks |
-| โณ | **์ง (Jin)** - *Thunder* | Sudden movement | ๐ฅ Explosive bursts |
-| โด | **์ (Son)** - *Wind* | Gentle persistence | ๐ช๏ธ Continuous pressure |
-| โต | **๊ฐ (Gam)** - *Water* | Adaptation and flow | ๐ก๏ธ Evasion and counters |
-| โถ | **๊ฐ (Gan)** - *Mountain* | Stillness and endurance | ๐ฟ Immovable defense |
-| โท | **๊ณค (Gon)** - *Earth* | Receptive yin energy | ๐ค Throws and takedowns |
+| Trigram | Name | Philosophy | Combat Style | Visual Effects |
+| :-----: | :-----------------------: | :---------------------: | :----------------------: | :----------------------------: |
+| โฐ | **๊ฑด (Geon)** - _Heaven_ | Pure yang energy | ๐ฅ Power strikes | Lightning bursts, screen shake |
+| โฑ | **ํ (Tae)** - _Lake_ | Joy and flexibility | ๐ Flowing combinations | Water ripple effects |
+| โฒ | **๋ฆฌ (Li)** - _Fire_ | Clarity and intensity | โก Fast, precise attacks | Fire particle trails |
+| โณ | **์ง (Jin)** - _Thunder_ | Sudden movement | ๐ฅ Explosive bursts | Thunder shock waves |
+| โด | **์ (Son)** - _Wind_ | Gentle persistence | ๐ช๏ธ Continuous pressure | Wind spiral effects |
+| โต | **๊ฐ (Gam)** - _Water_ | Adaptation and flow | ๐ก๏ธ Evasion and counters | Water shield barriers |
+| โถ | **๊ฐ (Gan)** - _Mountain_ | Stillness and endurance | ๐ฟ Immovable defense | Stone impact effects |
+| โท | **๊ณค (Gon)** - _Earth_ | Receptive yin energy | ๐ค Throws and takedowns | Earth shake tremors |
-### ๐ฏ Vital Point Targeting System
+### ๐ฅ Enhanced Combat Effects
-- **70 Anatomical Targets** across 5 body regions
-- **Precision-based damage** - Perfect hits deal 150% damage
-- **Real martial arts knowledge** - Learn actual vital points
-- **Anatomical mastery** - Unlock new techniques through study
+- **๐ Particle Systems** - Explosive hit effects with 25+ particles for vital strikes
+- **๐บ Screen Shake** - Dynamic camera shake based on impact force
+- **โก Lightning Effects** - Electric arcs for critical hits
+- **๐ญ Dark Trigram Logo** - Appears on devastating vital point strikes
+- **๐ Audio-Visual Sync** - Sound effects perfectly timed with visual impacts
+- **๐ Element-Specific Particles** - Each trigram has unique visual effects
-### ๐จ Authentic Korean Aesthetics
+### ๐ฏ Advanced Vital Point System
-- **Traditional Korean art style** with modern 2D animation
-- **Ink brush UI elements** and transitional effects
-- **Korean language integration** with proper martial arts terminology
-- **Cultural authenticity** in philosophy and combat principles
+- **๐ช 70 Anatomical Targets** with enhanced visual feedback
+- **๐ฏ Precision-based damage** - Perfect hits trigger spectacular effects
+- **๐ง Real martial arts knowledge** - Learn actual pressure points
+- **๐จ Korean Typography** - Beautiful Korean text integration
---
@@ -111,617 +117,198 @@ Built with modern web technologies for **maximum performance** and **security**:
-### ๐ฎ Game Engine
+### ๐ฎ Enhanced Game Engine
+



-### โก Performance
+### โก Performance Optimized
+



-### ๐ Security First
-
-
-
+### ๐จ Visual Effects
-
+
+
+
----
+
-## ๐ญ Game Modes
+### ๐จ New UI Components
-### ๐ฏ ์๋ จ ๋ชจ๋ (Suryeon Mode) - Training
-- **๐ Vital Point Study** - Learn anatomical targets
-- **โก Trigram Practice** - Master individual styles
-- **๐ฏ Precision Training** - Improve targeting accuracy
-- **๐ง Breathing Exercises** - Develop rhythm and timing
-
-### โ๏ธ ๋๋ จ ๋ชจ๋ (Daeryeon Mode) - Sparring
-- **๐ฅ Single Combat** - One-on-one precision duels
-- **๐ Tournament** - Progressive difficulty challenges
-- **๐ Master Challenges** - Face legendary martial artists
-
-### ๐ ์ฒ ํ ๋ชจ๋ (Cheolhak Mode) - Philosophy
-- **๐ I Ching Study** - Learn trigram meanings
-- **๐๏ธ Historical Lessons** - Korean martial arts heritage
-- **๐งโโ๏ธ Meditation Practice** - Improve focus and precision
-
-### ๐ ์์ด ๋ชจ๋ (Amsal Mode) - Assassination
-- **๐คซ Stealth Missions** - Eliminate without detection
-- **๐ฏ One-Strike Challenges** - Perfect precision required
-- **๐๏ธ Environmental Kills** - Use surroundings tactically
+- **CyberpunkPanel** - Futuristic panels with Korean design elements
+- **CyberpunkButton** - Interactive buttons with hover effects
+- **Enhanced HitEffects** - Spectacular visual feedback system
+- **Texture Loading** - Optimized asset management with caching
---
-## ๐ Security Features
-
-This template implements comprehensive security measures:
-
-- **๐ก๏ธ Supply Chain Security** - OSSF Scorecard analysis and dependency review
-- **๐ Static Analysis** - CodeQL scanning for vulnerabilities
-- **๐ฆ Dependency Protection** - Automated dependency vulnerability checks
-- **๐ Runner Hardening** - All CI/CD runners are hardened with audit logging
-- **๐ Security Policies** - GitHub security advisories and vulnerability reporting
-- **๐ท๏ธ Pinned Dependencies** - All GitHub Actions pinned to specific SHA hashes
-- **๐ SBOM Generation** - Software Bill of Materials for transparency
-- **๐ Build Attestations** - Cryptographic proof of build integrity
-- **๐ Artifact Verification** - SLSA-compliant build provenance
-- **๐ท๏ธ ZAP Security Scanning** - OWASP ZAP dynamic application security testing
-- **โก Lighthouse Performance** - Automated performance and accessibility audits
-
-## Features
-
-- โก **Vite** - Fast build tool and dev server
-- โ๏ธ **React 19** - Modern React with hooks
-- ๐ท **TypeScript** - Strict typing with latest standards
-- ๐งช **Vitest** - Fast unit testing with coverage
-- ๐ฒ **Cypress** - Reliable E2E testing
-- ๐ฆ **ESLint** - Code linting with TypeScript rules
-- ๐ **GitHub Actions** - Automated testing and reporting
-- ๐ฎ **PixiJS 8.x** - High-performance WebGL renderer for 2D games
-- ๐ต **Howler.js** - Audio library for games
-
-## Development Environment
-
-This template includes a fully configured development environment:
-
-- **๐ GitHub Codespaces** - Zero-configuration development environment
-- **๐ค GitHub Copilot** - AI-assisted development with code suggestions
-- **๐ฌ Copilot Chat** - In-editor AI assistance for debugging and explanations
-- **๐ง VS Code Extensions** - Pre-configured extensions for game development
-- **๐ Secure Container** - Hardened development container with security features
-
-### ๐ Codespaces Setup
-
-This repository is fully configured for GitHub Codespaces, providing:
-
-- **One-click setup** - Start coding immediately with zero configuration
-- **Pre-installed dependencies** - All tools and libraries ready to use
-- **Configured test environment** - Cypress and Vitest ready to run
-- **GitHub Copilot integration** - AI-powered code assistance
-- **Optimized performance** - Container configured for game development
-
-```mermaid
-graph LR
- A[Developer] -->|Opens in Codespace| B[Container Setup]
- B -->|Auto-configures| C[Development Environment]
- C -->|Provides| D[VS Code + Extensions]
- C -->|Initializes| E[Node.js Environment]
- C -->|Configures| F[Testing Tools]
-
- D -->|Includes| G[GitHub Copilot]
- D -->|Includes| H[ESLint Integration]
- D -->|Includes| I[Debug Tools]
-
- E -->|Installs| J[PixiJS 8.x]
- E -->|Installs| K[React 19]
- E -->|Installs| L[TypeScript]
-
- F -->|Prepares| M[Cypress E2E]
- F -->|Prepares| N[Vitest Unit Tests]
-
- G -->|Assists with| O[Game Logic]
- G -->|Suggests| P[Game Components]
-
- classDef primary fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000
- classDef tools fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px,color:#000
- classDef ai fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000
- classDef testing fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000
-
- class A,B,C primary
- class D,E,F tools
- class G,O,P ai
- class M,N testing
- class J,K,L tools
- class H,I tools
-```
+## ๐ฎ Game Controls
-## Security Workflows
-
-```mermaid
-graph TD
- A[๐ Code Push/PR] --> B{๐ก๏ธ Security Gates}
-
- B --> |๐ Code Analysis| C[CodeQL Scanning]
- B --> |๐ฆ Dependencies| D[Dependency Review]
- B --> |๐๏ธ Supply Chain| E[OSSF Scorecard]
-
- C --> |๐จ Vulnerabilities| F[Security Alerts]
- D --> |โ ๏ธ Known CVEs| F
- E --> |๐ Security Score| G[Security Dashboard]
-
- F --> H[๐ซ Block Merge]
- G --> I[โ
Security Badge]
-
- subgraph "๐ Protection Layers"
- J[Runner Hardening]
- K[Pinned Actions]
- L[Audit Logging]
- end
-
- subgraph "๐งช Runtime Security Testing"
- M[๐ท๏ธ ZAP DAST Scan]
- N[โก Lighthouse Audit]
- O[๐ Live Site Testing]
- end
-
- I --> M
- M --> |๐ Dynamic Scan| N
- N --> |๐ Performance Report| O
-
- %% Styling
- classDef security fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#000
- classDef analysis fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px,color:#000
- classDef protection fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000
- classDef alert fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#000
- classDef runtime fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#000
-
- class A,B,H security
- class C,D,E analysis
- class J,K,L protection
- class F,G,I alert
- class M,N,O runtime
-```
+### โจ๏ธ Keyboard Controls
-## Test & Report Workflow
+- **๐ Movement**: `WASD` or `Arrow Keys`
+- **โ๏ธ Trigram Techniques**: `1-8` (Number keys)
+- **๐ก๏ธ Block**: `Spacebar`
+- **๐ฏ Quick Attack**: `Mouse Click`
-```mermaid
-graph TD
- A[๐ Code Push/PR] --> B{๐ Prepare Environment}
+### ๐จ Visual Feedback
- B --> |โ
Dependencies| C[๐๏ธ Build Validation]
- B --> |โ
Cypress Cache| D[๐งช Unit Tests]
- B --> |โ
Display Setup| E[๐ E2E Tests]
+- **๐ฅ Hit Effects**: Dynamic particle explosions
+- **๐บ Screen Shake**: Impact-based camera movement
+- **โก Lightning**: Critical hit visual effects
+- **๐ฎ Logo Appearance**: Dark Trigram logo on vital strikes
- C --> |โ
Build Success| F{๐ Parallel Testing}
+---
- F --> D
- F --> E
+## ๐ญ Enhanced Game Modes
- D --> |๐ Coverage Report| G[๐ Test Reports]
- E --> |๐ฌ Videos & Screenshots| G
+### ๐ฏ ์๋ จ ๋ชจ๋ (Suryeon Mode) - Training
- G --> H[๐ค Artifact Upload]
- H --> I[โจ Combined Reports]
+- **๐ Vital Point Study** - Interactive anatomy lessons
+- **โก Trigram Practice** - Master visual effect combos
+- **๐ฏ Precision Training** - Enhanced targeting system
+- **๐ง Philosophy Study** - Learn I Ching principles
- %% Styling
- classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000
- classDef process fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000
- classDef test fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px,color:#000
- classDef report fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000
- classDef artifact fill:#fce4ec,stroke:#880e4f,stroke-width:2px,color:#000
+### โ๏ธ ๋๋ จ ๋ชจ๋ (Daeryeon Mode) - Enhanced Sparring
- class A,I startEnd
- class B,C,F process
- class D,E test
- class G,H report
- class H artifact
-```
+- **๐ฅ Visual Combat** - Spectacular effect-driven fights
+- **๐ Tournament** - Progressive visual complexity
+- **๐ Master Challenges** - Epic boss encounters with unique effects
-## Quick Start
+### ๐ ๊ธ์๊ฒฉ ๋ชจ๋ (Geupsogyeok Mode) - Vital Strike Training
-```bash
-# Using GitHub Codespaces
-# Click "Code" button on repository and select "Open with Codespaces"
+- **๐ฏ Precision Targeting** - Learn exact vital point locations
+- **๐ One-Shot Challenges** - Perfect accuracy required
+- **๐จ Effect Mastery** - Unlock new visual effects
-# Or local development:
-# Install dependencies
-npm install
+---
-# Start development server
-npm run dev
+## ๐ง Development Features
-# Build for production
-npm run build
+### ๐จ Enhanced Asset Pipeline
-# Run unit tests
-npm run test
+```typescript
+// Auto-loading textures with caching
+const { texture, loading, error } = useTexture("/dark-trigram-256.png");
-# Run E2E tests
-npm run test:e2e
+// Preload game assets
+await preloadGameTextures();
```
-## PixiJS 8.x Integration
-
-This template uses PixiJS 8.x for high-performance 2D game rendering:
-
-- Modern WebGL-based rendering
-- Optimized sprite batching
-- Integrated with React via @pixi/react
-- Sound support via @pixi/sound and Howler.js
-- Responsive game canvas
-- Touch and mouse input handling
-
-Example game component:
-
-```tsx
-import { Stage, Sprite, useTick } from "@pixi/react";
-import { useState } from "react";
-
-export function Game() {
- const [position, setPosition] = useState({ x: 100, y: 100 });
-
- useTick((delta) => {
- // Game logic here
- setPosition((prev) => ({
- x: prev.x + delta,
- y: prev.y,
- }));
- });
-
- return (
-
-
-
- );
-}
-```
+### ๐ฅ Particle Effect System
-## Testing
-
-### Unit Tests
-
-- Uses Vitest with jsdom environment
-- Configured for React Testing Library
-- Coverage reports generated automatically
-- Run with: `npm run test`
-
-### E2E Tests
-
-- Uses Cypress for end-to-end testing
-- Starts dev server automatically
-- Screenshots and videos on failure
-- Run with: `npm run test:e2e`
-
-### CI/CD Pipeline
-
-```mermaid
-flowchart LR
- subgraph "๐ง CI Pipeline"
- A1[๐ Code Changes] --> A2[๐ Lint & Type Check]
- A2 --> A3[๐๏ธ Build]
- A3 --> A4[๐งช Test]
- A4 --> A5[๐ Report]
- end
-
- subgraph "๐ Security Pipeline"
- S1[๐ก๏ธ CodeQL Analysis]
- S2[๐ฆ Dependency Review]
- S3[๐ OSSF Scorecard]
- S4[๐ Runner Hardening]
- end
-
- subgraph "๐ Test Coverage"
- B1[Unit Tests 80%+ Coverage]
- B2[E2E Tests Critical Flows]
- B3[Type Safety Strict Mode]
- end
-
- subgraph "๐ฏ Outputs"
- C1[๐ Coverage Reports]
- C2[๐ฌ Test Videos]
- C3[๐ธ Screenshots]
- C4[๐ JUnit XML]
- C5[๐ก๏ธ Security Reports]
- end
-
- A4 --> B1
- A4 --> B2
- A4 --> B3
-
- A1 --> S1
- A1 --> S2
- A1 --> S3
- A1 --> S4
-
- A5 --> C1
- A5 --> C2
- A5 --> C3
- A5 --> C4
- S1 --> C5
- S2 --> C5
- S3 --> C5
-
- %% Styling
- classDef pipeline fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
- classDef security fill:#ffebee,stroke:#c62828,stroke-width:2px
- classDef testing fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
- classDef output fill:#fff8e1,stroke:#f57c00,stroke-width:2px
-
- class A1,A2,A3,A4,A5 pipeline
- class S1,S2,S3,S4 security
- class B1,B2,B3 testing
- class C1,C2,C3,C4,C5 output
+```typescript
+// Generate hit particles with Korean aesthetics
+const particles = generateHitParticles({
+ count: isVitalHit ? 25 : 12,
+ colors: [CYAN, ORANGE, LIGHTNING_YELLOW],
+ trigram: effect.trigram,
+});
```
-### Security Workflows
-
-- **CodeQL Analysis**: Automated vulnerability scanning on push/PR
-- **Dependency Review**: Checks for known vulnerabilities in dependencies
-- **OSSF Scorecard**: Supply chain security assessment with public scoring
-- **Runner Hardening**: All CI/CD runners use hardened security policies
-
-## ๐ Release Management
-
-This template includes a comprehensive, security-first release workflow with automated versioning, security attestations, and deployment.
-
-### Release Flow
-
-```mermaid
-flowchart TD
- A[๐ Release Trigger] --> B{๐ Release Type}
-
- B -->|๐ท๏ธ Tag Push| C[๐ Automatic Release]
- B -->|โก Manual Dispatch| D[๐ Manual Release]
-
- C --> E[๐ฆ Prepare Phase]
- D --> E
-
- E --> F[๐๏ธ Build & Test]
- F --> G[๐ Security Validation]
-
- G --> H[๐ Generate SBOM]
- H --> I[๐ Create Attestations]
- I --> J[๐ Draft Release Notes]
-
- J --> K[๐ Deploy to Pages]
- K --> L[๐ข Publish Release]
-
- subgraph "๐ Security Layers"
- M[SLSA Build Provenance]
- N[SBOM Attestation]
- O[Artifact Signing]
- P[Supply Chain Verification]
- end
+### ๐บ Screen Effects
- I --> M
- I --> N
- I --> O
- G --> P
-
- %% Styling
- classDef trigger fill:#e1f5fe,stroke:#01579b,stroke-width:2px
- classDef process fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
- classDef security fill:#ffebee,stroke:#c62828,stroke-width:2px
- classDef deploy fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
-
- class A,B,C,D trigger
- class E,F,J,K,L process
- class G,H,I,M,N,O,P security
+```typescript
+// Dynamic screen shake based on impact
+generateScreenShake({
+ intensity: isVitalHit ? 15 : damage / 2,
+ duration: 300,
+});
```
-### ๐ท๏ธ Release Types
-
-#### Automatic Releases (Tag-based)
-
-```bash
-# Create and push a tag to trigger automatic release
-git tag v1.0.0
-git push origin v1.0.0
-```
-
-#### Manual Releases (Workflow Dispatch)
-
-- Navigate to **Actions** โ **Build, Attest and Release**
-- Click **Run workflow**
-- Specify version (e.g., `v1.0.1`) and pre-release status
-- The workflow handles version bumping and tagging automatically
-
-### ๐ Automated Release Notes
-
-Release notes are automatically generated using semantic labeling:
-
-```mermaid
-graph LR
- A[๐ PR Labels] --> B[๐ Release Drafter]
- B --> C[๐ Categorized Notes]
+---
- subgraph "๐ท๏ธ Label Categories"
- D[๐ New Features]
- E[๐ฎ Game Development]
- F[๐ Security & Compliance]
- G[๐ Bug Fixes]
- H[๐ฆ Dependencies]
- I[๐งช Test Coverage]
- end
+## ๐ Quick Start
- A --> D
- A --> E
- A --> F
- A --> G
- A --> H
- A --> I
+### ๐ Play Online
- C --> J[๐ข GitHub Release]
+**[๐ฎ Play Black Trigram Now](https://hack23.github.io/blacktrigram/)**
- classDef labels fill:#fff3e0,stroke:#e65100,stroke-width:2px
- classDef process fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
- classDef output fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
+### ๐ง Local Development
- class D,E,F,G,H,I labels
- class A,B,C process
- class J output
-```
+```bash
+# Clone repository
+git clone https://github.com/Hack23/blacktrigram.git
+cd blacktrigram
-#### Release Note Categories
-
-- **๐ New Features** - Major feature additions
-- **๐ฎ Game Development** - Game logic, graphics, audio improvements
-- **๐จ UI/UX Improvements** - Interface and design updates
-- **๐๏ธ Infrastructure & Performance** - Build and performance optimizations
-- **๐ Code Quality & Refactoring** - Code improvements and testing
-- **๐ Security & Compliance** - Security updates and fixes
-- **๐ Documentation** - Documentation improvements
-- **๐ฆ Dependencies** - Dependency updates
-- **๐ Bug Fixes** - Bug fixes and patches
-
-### ๐ Security Attestations & SBOM
-
-#### Software Bill of Materials (SBOM)
-
-Every release includes a comprehensive SBOM in SPDX format:
-
-```json
-{
- "SPDXID": "SPDXRef-DOCUMENT",
- "name": "game-v1.0.0",
- "packages": [
- {
- "SPDXID": "SPDXRef-Package-react",
- "name": "react",
- "versionInfo": "19.1.0",
- "licenseConcluded": "MIT"
- }
- ]
-}
-```
+# Install dependencies
+npm install
-#### Build Provenance Attestations
-
-SLSA-compliant build attestations provide cryptographic proof:
-
-```json
-{
- "_type": "https://in-toto.io/Statement/v0.1",
- "predicateType": "https://slsa.dev/provenance/v0.2",
- "subject": [
- {
- "name": "game-v1.0.0.zip",
- "digest": {
- "sha256": "abc123..."
- }
- }
- ],
- "predicate": {
- "builder": {
- "id": "https://github.com/actions/runner"
- },
- "buildType": "https://github.com/actions/workflow@v1"
- }
-}
-```
+# Start development server with hot reload
+npm run dev
-#### Verification Commands
+# Build for production with optimizations
+npm run build
-```bash
-# Verify build provenance
-gh attestation verify game-v1.0.0.zip \
- --owner Hack23 --repo game
-
-# Verify SBOM attestation
-gh attestation verify game-v1.0.0.zip \
- --owner Hack23 --repo game \
- --predicate-type https://spdx.dev/Document
+# Run comprehensive test suite
+npm run test
+npm run test:e2e
```
-### ๐ฆ Release Artifacts
+### ๐ณ GitHub Codespaces
-Each release includes multiple artifacts with full traceability:
-
-```
-๐ฆ Release v1.0.0
-โโโ ๐ฎ game-v1.0.0.zip # Built application
-โโโ ๐ game-v1.0.0.spdx.json # Software Bill of Materials
-โโโ ๐ game-v1.0.0.zip.intoto.jsonl # Build provenance attestation
-โโโ ๐ game-v1.0.0.spdx.json.intoto.jsonl # SBOM attestation
-```
+1. Click **"Use this template"** or fork the repository
+2. Click **"Code"** โ **"Create codespace on main"**
+3. Wait for automatic setup (2-3 minutes)
+4. Run `npm run dev` and start coding!
-### ๐ Deployment Pipeline
+---
-```mermaid
-sequenceDiagram
- participant Dev as ๐จโ๐ป Developer
- participant GH as ๐ GitHub
- participant CI as ๐ CI/CD
- participant Sec as ๐ Security
- participant Pages as ๐ GitHub Pages
+## ๐ฏ Game Philosophy
- Dev->>GH: ๐ท๏ธ Push Tag/Manual Trigger
- GH->>CI: ๐ Start Release Workflow
+> **"ํ๊ด์์ ๋ฌด์๋ ๋จ์ํ ์ธ์์ด ์๋๋ค. ๊ทธ๊ฒ์ ์ฐ์ฃผ์ ์กฐํ๋ฅผ ๋ชธ์ผ๋ก ํํํ๋ ์์ ์ด๋ค."**
+>
+> _"In Black Trigram, martial arts are not mere fighting. They are the art of expressing cosmic harmony through the body."_
- CI->>CI: ๐งช Run Tests & Build
- CI->>Sec: ๐ Security Scans
- Sec-->>CI: โ
Security Validated
+### ๐ฎ The Eight Paths (ํ๊ด๋)
- CI->>Sec: ๐ Generate SBOM
- CI->>Sec: ๐ Create Attestations
- Sec-->>CI: ๐ Security Artifacts Ready
+Each trigram represents not just a fighting style, but a complete philosophy:
- CI->>GH: ๐ Draft Release Notes
- CI->>GH: ๐ฆ Upload Artifacts
+- **โฐ Heaven (๊ฑด)** - Leadership through overwhelming force
+- **โฑ Lake (ํ)** - Joy in fluid, adaptive combat
+- **โฒ Fire (๋ฆฌ)** - Clarity of purpose in every strike
+- **โณ Thunder (์ง)** - Decisive action without hesitation
+- **โด Wind (์)** - Persistent pressure that wears down mountains
+- **โต Water (๊ฐ)** - Flowing around obstacles to find weakness
+- **โถ Mountain (๊ฐ)** - Immovable defense, perfect timing
+- **โท Earth (๊ณค)** - Receptive power that grounds opponents
- CI->>Pages: ๐ Deploy Application
- Pages-->>CI: โ
Deployment Success
+---
- CI->>GH: ๐ข Publish Release
- GH-->>Dev: ๐ Release Complete
-```
+## ๐ Achievements & Mastery
-### ๐ Security Compliance
+### ๐จ Visual Effect Mastery
-#### OSSF Scorecard Integration
+- **๐ซ Particle Master** - Execute 100 perfect vital strikes
+- **โก Lightning Bringer** - Land 50 critical hits in a row
+- **๐ Trigram Sage** - Master all 8 visual effect styles
+- **๐ Dark Trigram Legend** - Achieve flawless victory with logo appearance
-- **Automated scoring** of supply chain security practices
-- **Public transparency** with security badge
-- **Continuous monitoring** of security posture
+### ๐ฏ Combat Excellence
-#### Supply Chain Protection
+- **๐ช Vital Point Scholar** - Identify all 70 pressure points
+- **๐ฅท Shadow Strike** - Win without taking damage
+- **๐ฅ Combo Master** - Chain 20+ techniques seamlessly
+- **๐ฎ Philosophy Student** - Complete I Ching study mode
-- **Pinned dependencies** - All GitHub Actions pinned to SHA hashes
-- **Dependency scanning** - Automated vulnerability detection
-- **SLSA compliance** - Build integrity and provenance
-- **Signed artifacts** - Cryptographic verification of releases
+---
-### ๐ Release Metrics
+
-Track release quality and security with built-in metrics:
+## ๐ Ready to Master the Dark Arts?
-- **๐ Security Score** - OSSF Scorecard rating
-- **๐ Test Coverage** - Unit and E2E test coverage
-- **๐ท๏ธ Vulnerability Count** - Known security issues
-- **๐ฆ Dependency Health** - Outdated/vulnerable dependencies
-- **๐ Build Success Rate** - CI/CD pipeline reliability
+**[๐ฎ Begin Your Journey](https://hack23.github.io/blacktrigram/)**
-## Building Your Game
+_Experience the harmony of body, mind, and spectacular visual effects_
-This template provides a **secure foundation** for game development:
+---
-1. Replace the counter example with your game logic
-2. Add game-specific components in `src/components/`
-3. Create game state management (Context API, Zustand, etc.)
-4. Add unit tests for game logic
-5. Create E2E tests for game flows
-6. **Create releases** using the automated workflow
-7. **Monitor security** through OSSF Scorecard and attestations
-8. Deploy using the included **security-hardened** GitHub Actions
+### Built with โค๏ธ and โก by the Dark Trigram Team
-All security workflows will automatically protect your game from common vulnerabilities and supply chain attacks, while providing full transparency through SBOM and attestations.
+**๐ฅ ๋ฌด์์ ๊ธธ์ ๊ฑธ์ด๋ผ (Walk the Path of Martial Arts) ๐ฅ**
-Happy gaming! ๐ฎ๐
+
diff --git a/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- Error Recovery -- should recover from rapid key combinations (failed).png b/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- Error Recovery -- should recover from rapid key combinations (failed).png
new file mode 100644
index 00000000000..afb4b358dbe
Binary files /dev/null and b/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- Error Recovery -- should recover from rapid key combinations (failed).png differ
diff --git a/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- should recover from rapid key combinations -- before each hook (failed) (attempt 2).png b/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- should recover from rapid key combinations -- before each hook (failed) (attempt 2).png
new file mode 100644
index 00000000000..904802a7b1e
Binary files /dev/null and b/cypress/screenshots/game-flow.cy.ts/Black Trigram - Complete Game Flow E2E -- should recover from rapid key combinations -- before each hook (failed) (attempt 2).png differ
diff --git a/cypress/videos/app.cy.ts.mp4 b/cypress/videos/app.cy.ts.mp4
new file mode 100644
index 00000000000..74c41f80382
Binary files /dev/null and b/cypress/videos/app.cy.ts.mp4 differ
diff --git a/cypress/videos/game-flow.cy.ts.mp4 b/cypress/videos/game-flow.cy.ts.mp4
new file mode 100644
index 00000000000..d2f461e9cee
Binary files /dev/null and b/cypress/videos/game-flow.cy.ts.mp4 differ
diff --git a/index.html b/index.html
index ea50d2bfe67..38c83c1c68f 100644
--- a/index.html
+++ b/index.html
@@ -10,17 +10,17 @@
content="Master the dark arts of vital point striking through eight distinct trigram fighting styles in this precision combat experience."
/>
-
+
-
+
{
Container: expect.any(Function),
Graphics: expect.any(Function),
Text: expect.any(Function),
+ Sprite: expect.any(Function),
});
});
});
diff --git a/src/App.tsx b/src/App.tsx
index 0a257323760..5b564750a06 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,16 +1,18 @@
import { Application, extend } from "@pixi/react";
-import { Container, Graphics, Text } from "pixi.js";
+import { Container, Graphics, Text, Sprite } from "pixi.js";
import { useState, useEffect, useCallback } from "react";
import type { JSX } from "react";
import "./App.css";
import { GameEngine } from "./components/game/GameEngine";
import { useAudio } from "./audio/AudioManager";
+import { useTexture } from "./hooks/useTexture";
// Extend @pixi/react with the Pixi components we want to use
extend({
Container,
Graphics,
Text,
+ Sprite,
});
type GameMode = "intro" | "game" | "training";
@@ -91,6 +93,8 @@ function IntroScreen({
"sparring"
);
const audio = useAudio();
+ const [initialized, setInitialized] = useState(false);
+ const { texture: logoTexture } = useTexture("/dark-trigram-256.png");
// Enhanced trigram positioning with combat style icons
const trigrams: TrigramSymbol[] = [
@@ -188,12 +192,23 @@ function IntroScreen({
const interval = setInterval(() => {
setTime((prev) => prev + 0.016); // ~60fps
}, 16);
+
+ // Initialize controls state to prevent first-time issues
+ if (!initialized) {
+ setInitialized(true);
+ }
+
return () => clearInterval(interval);
- }, []);
+ }, [initialized]);
- // Keyboard controls for intro screen
+ // Keyboard controls for intro screen with improved initialization
useEffect(() => {
+ if (!initialized) return;
+
const handleKeyDown = (event: KeyboardEvent): void => {
+ // Ensure control signals are properly triggered
+ event.preventDefault();
+
// Navigation controls
if (event.code === "ArrowLeft" || event.code === "KeyA") {
audio.playSFX("menu_hover");
@@ -206,7 +221,7 @@ function IntroScreen({
// Action controls
if (event.code === "Space" || event.code === "Enter") {
- event.preventDefault();
+ audio.playSFX("menu_select");
if (selectedOption === "sparring") {
onStartGame();
} else {
@@ -216,31 +231,31 @@ function IntroScreen({
// Alternative keys
if (event.code === "AltLeft" || event.code === "AltRight") {
- event.preventDefault();
+ audio.playSFX("menu_select");
onStartTraining();
}
// Quick start with number keys
if (event.code === "Digit1") {
+ audio.playSFX("menu_select");
onStartGame();
}
if (event.code === "Digit2") {
+ audio.playSFX("menu_select");
onStartTraining();
}
};
- const handleKeyUp = (): void => {
- // Key up handler - no state tracking needed for intro screen
- };
-
window.addEventListener("keydown", handleKeyDown);
- window.addEventListener("keyup", handleKeyUp);
+
+ // Ensure app has focus to receive key events
+ window.focus();
+ document.body.click();
return () => {
window.removeEventListener("keydown", handleKeyDown);
- window.removeEventListener("keyup", handleKeyUp);
};
- }, [selectedOption, onStartGame, onStartTraining, audio]);
+ }, [selectedOption, onStartGame, onStartTraining, audio, initialized]);
const drawBackground = useCallback((graphics: Graphics) => {
graphics.clear();
@@ -249,8 +264,8 @@ function IntroScreen({
graphics.rect(0, 0, window.innerWidth, window.innerHeight);
graphics.fill();
- // Subtle grid pattern for traditional Korean aesthetic
- graphics.setStrokeStyle({ color: 0x111111, width: 1, alpha: 0.3 });
+ // Cyan grid pattern
+ graphics.setStrokeStyle({ color: 0x003333, width: 1, alpha: 0.3 });
const gridSize = 50;
for (let x = 0; x < window.innerWidth; x += gridSize) {
graphics.moveTo(x, 0);
@@ -268,31 +283,21 @@ function IntroScreen({
(graphics: Graphics) => {
graphics.clear();
- // Outer ring - traditional Korean red (๋จ์)
- graphics.setStrokeStyle({ color: 0x8b0000, width: 4 });
+ // Cyan outer ring
+ graphics.setStrokeStyle({ color: 0x00ffd0, width: 4 });
graphics.circle(0, 0, 140);
graphics.stroke();
- // Middle ring - white for balance
- graphics.setStrokeStyle({ color: 0xffffff, width: 2 });
+ // Inner cyan ring
+ graphics.setStrokeStyle({ color: 0x00ffd0, width: 2, alpha: 0.7 });
graphics.circle(0, 0, 120);
graphics.stroke();
- // Inner ring - darker red
- graphics.setStrokeStyle({ color: 0x660000, width: 2 });
- graphics.circle(0, 0, 100);
- graphics.stroke();
-
- // Center yin-yang inspired circle - pulsing
+ // Pulsing cyan center
const pulse = Math.sin(time * 1.5) * 0.4 + 0.6;
- graphics.setFillStyle({ color: 0x8b0000, alpha: pulse });
+ graphics.setFillStyle({ color: 0x00ffd0, alpha: pulse });
graphics.circle(0, 0, 12);
graphics.fill();
-
- // Small white dot for balance
- graphics.setFillStyle({ color: 0xffffff, alpha: pulse });
- graphics.circle(3, 0, 3);
- graphics.fill();
},
[time]
);
@@ -342,45 +347,61 @@ function IntroScreen({
- {/* Enhanced main title with iconic elements */}
+ {/* Dark Trigram Logo */}
{
g.clear();
- // Title background with traditional design
- g.setFillStyle({ color: 0x000000, alpha: 0.8 });
- g.roundRect(-250, -50, 500, 100, 20);
+ // Dark background with deeper panel
+ g.setFillStyle({ color: 0x000a12, alpha: 0.95 });
+ g.roundRect(-270, -70, 540, 140, 20);
g.fill();
- // Golden border
- g.setStrokeStyle({ color: 0xffd700, width: 3 });
- g.roundRect(-250, -50, 500, 100, 20);
+ // Cyan glowing border with double-line effect
+ g.setStrokeStyle({ color: 0x00ffd0, width: 3 });
+ g.roundRect(-270, -70, 540, 140, 20);
g.stroke();
- }}
- />
-
+ {logoTexture && (
+
+ )}
+
@@ -575,7 +596,7 @@ function IntroScreen({
{/* Enhanced game mode selection with better visual feedback */}
@@ -766,6 +830,7 @@ function TrainingMode(): JSX.Element {
}
function BackButton({ onBack }: { onBack: () => void }): JSX.Element {
+ const [isHovered, setIsHovered] = useState(false);
const audio = useAudio();
return (
@@ -778,23 +843,52 @@ function BackButton({ onBack }: { onBack: () => void }): JSX.Element {
audio.playSFX("menu_back");
onBack();
}}
- onPointerEnter={() => audio.playSFX("menu_hover")}
+ onPointerEnter={() => {
+ audio.playSFX("menu_hover");
+ setIsHovered(true);
+ }}
+ onPointerLeave={() => setIsHovered(false)}
>
{
g.clear();
- g.setFillStyle({ color: 0x666666 });
- g.roundRect(-30, -15, 60, 30, 5);
+ // Cyberpunk back button
+ g.setFillStyle({ color: 0x000a12, alpha: 0.9 });
+ g.roundRect(-40, -20, 80, 40, 5);
g.fill();
+
+ // Border
+ g.setStrokeStyle({
+ color: isHovered ? 0x00ffd0 : 0x004455,
+ width: isHovered ? 2 : 1,
+ alpha: isHovered ? 0.9 : 0.7,
+ });
+ g.roundRect(-40, -20, 80, 40, 5);
+ g.stroke();
+
+ // Tech accent lines
+ g.setStrokeStyle({ color: 0x00ffd0, width: 1, alpha: 0.4 });
+ g.moveTo(-40, -10);
+ g.lineTo(-30, -20);
+ g.moveTo(40, -10);
+ g.lineTo(30, -20);
+ g.stroke();
}}
/>
diff --git a/src/Game.css b/src/Game.css
new file mode 100644
index 00000000000..9c42fe50b27
--- /dev/null
+++ b/src/Game.css
@@ -0,0 +1,121 @@
+/* Game-specific styling for Dark Trigram */
+
+/* Focus outline removal for better game aesthetics */
+canvas:focus {
+ outline: none !important;
+}
+
+/* Loading message styling - cyberpunk theme */
+.loading-screen {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ color: #00ffd0;
+ font-family: "Orbitron", "Noto Sans KR", monospace;
+}
+
+.loading-logo {
+ width: 150px;
+ height: 150px;
+ margin-bottom: 20px;
+ animation: pulse 2s infinite;
+}
+
+.loading-text {
+ font-size: 18px;
+ letter-spacing: 2px;
+ margin-top: 20px;
+ text-transform: uppercase;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid rgba(0, 255, 208, 0.3);
+ border-top: 4px solid #00ffd0;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.6;
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+/* Vital point targeting effect */
+.vital-point {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border: 1px solid #00ffd0;
+ transform: translate(-50%, -50%);
+ opacity: 0.7;
+ animation: target-pulse 2s infinite;
+}
+
+.vital-point::before,
+.vital-point::after {
+ content: "";
+ position: absolute;
+ background-color: #00ffd0;
+}
+
+.vital-point::before {
+ top: 50%;
+ left: -5px;
+ right: -5px;
+ height: 1px;
+ transform: translateY(-50%);
+}
+
+.vital-point::after {
+ left: 50%;
+ top: -5px;
+ bottom: -5px;
+ width: 1px;
+ transform: translateX(-50%);
+}
+
+@keyframes target-pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(0, 255, 208, 0.4);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(0, 255, 208, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(0, 255, 208, 0);
+ }
+}
+
+/* Neon text effect for Korean text */
+.neon-text {
+ color: #fff;
+ text-shadow: 0 0 5px #00ffd0, 0 0 10px #00ffd0, 0 0 20px #00ffd0,
+ 0 0 40px #00ffd0;
+ font-weight: bold;
+}
diff --git a/src/components/game/DojangBackground.tsx b/src/components/game/DojangBackground.tsx
index 97a81d3a1b0..a54d748516e 100644
--- a/src/components/game/DojangBackground.tsx
+++ b/src/components/game/DojangBackground.tsx
@@ -1,269 +1,279 @@
+import { useState, useEffect, useCallback } from "react";
import type { JSX } from "react";
import type { Graphics as PixiGraphics } from "pixi.js";
+import { useTexture } from "../../hooks/useTexture";
+
+// Dark Trigram theme constants with strict typing
+const DARK_TRIGRAM_THEME = {
+ PRIMARY_CYAN: 0x00ffd0,
+ DARK_BG: 0x0a0e12,
+ DARKER_BG: 0x181c20,
+ MEDIUM_BG: 0x23272b,
+ VITAL_ORANGE: 0xff4400,
+ CRITICAL_RED: 0xff3030,
+ WHITE: 0xffffff,
+ GRID_CYAN: 0x003333,
+} as const;
interface DojangBackgroundProps {
readonly gameTime: number;
}
+interface VitalPoint {
+ readonly id: number;
+ readonly x: number;
+ readonly y: number;
+ readonly damage: number;
+ readonly type: string;
+}
+
+interface TrigramPosition {
+ readonly x: number;
+ readonly y: number;
+ readonly symbol: string;
+ readonly name: string;
+}
+
export function DojangBackground({
gameTime,
}: DojangBackgroundProps): JSX.Element {
- const drawDojang = (graphics: PixiGraphics): void => {
- graphics.clear();
-
- // Enhanced traditional Korean dojang with better visual hierarchy
- graphics.setFillStyle({ color: 0x000000 });
- graphics.rect(0, 0, window.innerWidth, window.innerHeight);
- graphics.fill();
-
- // Improved animated background particles with icon-like shapes
- for (let i = 0; i < 15; i++) {
- const x =
- (Math.sin(gameTime * 0.01 + i) * 150 + window.innerWidth / 2) %
- window.innerWidth;
- const y =
- (Math.cos(gameTime * 0.008 + i * 0.5) * 150 + window.innerHeight / 2) %
- window.innerHeight;
- const alpha = Math.sin(gameTime * 0.02 + i) * 0.08 + 0.08;
-
- // Create star-like particles representing martial energy
- graphics.setFillStyle({ color: 0x8b0000, alpha });
- const size = 3 + Math.sin(gameTime * 0.03 + i) * 2;
- graphics.circle(x, y, size);
- graphics.fill();
-
- // Add cross-shaped energy markers
- graphics.setStrokeStyle({
- color: 0xffd700,
- width: 1,
- alpha: alpha * 0.6,
+ const [vitalPoints, setVitalPoints] = useState([]);
+ const { texture: logoTexture } = useTexture("/dark-trigram-256.png");
+
+ // Initialize vital points for Korean martial arts atmosphere
+ useEffect(() => {
+ const vitalPointTypes: readonly string[] = [
+ "sternum",
+ "solar_plexus",
+ "throat",
+ "temples",
+ "pressure_points",
+ "nerve_clusters",
+ "circulation_points",
+ "balance_points",
+ ] as const;
+
+ const newVitalPoints: VitalPoint[] = [];
+
+ for (let i = 0; i < 12; i++) {
+ const randomIndex = Math.floor(Math.random() * vitalPointTypes.length);
+ const vitalPointType = vitalPointTypes[randomIndex] ?? "pressure_points";
+
+ newVitalPoints.push({
+ id: i,
+ x: Math.random() * window.innerWidth,
+ y: Math.random() * window.innerHeight,
+ damage: Math.random() * 50 + 10,
+ type: vitalPointType,
});
- graphics.moveTo(x - size, y);
- graphics.lineTo(x + size, y);
- graphics.moveTo(x, y - size);
- graphics.lineTo(x, y + size);
- graphics.stroke();
}
-
- // Enhanced traditional Korean flooring with geometric patterns
- const floorHeight = window.innerHeight * 0.35;
- graphics.setFillStyle({ color: 0x4a3728 }); // Darker wood tone
- graphics.rect(
- 0,
- window.innerHeight - floorHeight,
- window.innerWidth,
- floorHeight
- );
- graphics.fill();
-
- // Enhanced wood grain pattern with Korean aesthetic and subtle animation
- graphics.setStrokeStyle({ color: 0x654321, width: 1, alpha: 0.8 });
- for (let x = 0; x < window.innerWidth; x += 60) {
- const wobble = Math.sin(gameTime * 0.01 + x * 0.01) * 2;
- graphics.moveTo(x + wobble, window.innerHeight - floorHeight);
- graphics.lineTo(x + wobble, window.innerHeight);
- graphics.stroke();
-
- // Add traditional Korean wood joining details with subtle glow
- graphics.setFillStyle({
- color: 0x2a1f1a,
- alpha: 0.6 + Math.sin(gameTime * 0.005 + x) * 0.1,
- });
- graphics.rect(
- x - 2 + wobble,
- window.innerHeight - floorHeight,
- 4,
- floorHeight
- );
+
+ setVitalPoints(newVitalPoints);
+ }, []);
+
+ // Enhanced dojang drawing with Korean martial arts elements
+ const drawDojang = useCallback(
+ (graphics: PixiGraphics): void => {
+ graphics.clear();
+
+ // Deep black background
+ graphics.setFillStyle({ color: DARK_TRIGRAM_THEME.DARK_BG });
+ graphics.rect(0, 0, window.innerWidth, window.innerHeight);
graphics.fill();
- }
-
- // Enhanced fighting arena with iconic Korean border patterns
- const arenaWidth = Math.min(700, window.innerWidth * 0.8);
- const arenaHeight = Math.min(400, window.innerHeight * 0.5);
- const arenaX = (window.innerWidth - arenaWidth) / 2;
- const arenaY = (window.innerHeight - arenaHeight) / 2;
-
- // Traditional Korean taeguk-inspired border design
- const borderPulse = Math.sin(gameTime * 0.02) * 0.2 + 0.8;
-
- // Outer ceremonial border with traditional patterns
- graphics.setStrokeStyle({ color: 0x8b0000, width: 8, alpha: borderPulse });
- graphics.rect(arenaX - 50, arenaY - 40, arenaWidth + 100, arenaHeight + 80);
- graphics.stroke();
- // Traditional Korean decorative corners (icon-like elements)
- const cornerPositions: Array<[number, number]> = [
- [arenaX - 50, arenaY - 40],
- [arenaX + arenaWidth + 50, arenaY - 40],
- [arenaX - 50, arenaY + arenaHeight + 40],
- [arenaX + arenaWidth + 50, arenaY + arenaHeight + 40],
- ];
+ // Subtle animated grid pattern
+ graphics.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.GRID_CYAN,
+ width: 1,
+ alpha: 0.03
+ });
+
+ const gridSize = 40;
+ for (let y = 0; y < window.innerHeight; y += gridSize) {
+ graphics.moveTo(0, y);
+ graphics.lineTo(window.innerWidth, y);
+ graphics.stroke();
+ }
+
+ for (let x = 0; x < window.innerWidth; x += gridSize) {
+ graphics.moveTo(x, 0);
+ graphics.lineTo(x, window.innerHeight);
+ graphics.stroke();
+ }
- cornerPositions.forEach(([x, y]) => {
- // Traditional Korean corner decoration (resembling ํ๊ทน symbol elements)
- graphics.setFillStyle({ color: 0xffd700, alpha: borderPulse * 0.8 });
- graphics.circle(x, y, 12);
- graphics.fill();
+ // Central octagonal dojang platform
+ const centerX = window.innerWidth / 2;
+ const centerY = window.innerHeight / 2;
+ const radius = Math.min(window.innerWidth, window.innerHeight) * 0.25;
- graphics.setFillStyle({ color: 0x8b0000, alpha: borderPulse });
- graphics.circle(x + 3, y, 6);
+ // Platform base
+ graphics.setFillStyle({ color: DARK_TRIGRAM_THEME.DARKER_BG });
+ graphics.circle(centerX, centerY, radius);
graphics.fill();
- });
-
- // Enhanced center yin-yang inspired design with iconic elements
- const centerX = window.innerWidth / 2;
- const centerY = window.innerHeight / 2;
-
- // Main ceremonial circle with traditional Korean colors
- graphics.setStrokeStyle({ color: 0x8b0000, width: 6, alpha: borderPulse });
- graphics.circle(centerX, centerY, 80 + Math.sin(gameTime * 0.015) * 8);
- graphics.stroke();
- // Secondary ring with decorative elements
- graphics.setStrokeStyle({
- color: 0xffd700,
- width: 3,
- alpha: borderPulse * 0.9,
- });
- graphics.circle(centerX, centerY, 60 + Math.cos(gameTime * 0.018) * 5);
- graphics.stroke();
-
- // Central energy core with pulsing effect
- const corePulse = Math.sin(gameTime * 0.025) * 0.4 + 0.7;
- graphics.setFillStyle({ color: 0x8b0000, alpha: corePulse });
- graphics.circle(centerX, centerY, 12 + Math.sin(gameTime * 0.03) * 3);
- graphics.fill();
+ // Traditional Korean octagonal platform outline
+ graphics.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 2,
+ alpha: 0.6
+ });
+
+ const octagonPoints: Array<{ x: number; y: number }> = [];
+ for (let i = 0; i < 8; i++) {
+ const angle = (i * Math.PI) / 4 - Math.PI / 8;
+ octagonPoints.push({
+ x: centerX + Math.cos(angle) * radius,
+ y: centerY + Math.sin(angle) * radius,
+ });
+ }
- // Traditional Korean trigram-inspired orbital elements
- for (let i = 0; i < 8; i++) {
- const angle = gameTime * 0.008 + (i * Math.PI) / 4;
- const orbitRadius = 100 + Math.sin(gameTime * 0.01 + i) * 15;
- const orbX = centerX + Math.cos(angle) * orbitRadius;
- const orbY = centerY + Math.sin(angle) * orbitRadius;
+ // Draw octagon outline
+ if (octagonPoints.length > 0) {
+ const firstPoint = octagonPoints[0];
+ if (firstPoint) {
+ graphics.moveTo(firstPoint.x, firstPoint.y);
+ for (let i = 1; i < 8; i++) {
+ const point = octagonPoints[i];
+ if (point) {
+ graphics.lineTo(point.x, point.y);
+ }
+ }
+ graphics.closePath();
+ graphics.stroke();
+ }
+ }
- // Create trigram-line inspired orbital markers
+ // Pulsing center core with yin-yang inspiration
+ const corePulse = Math.sin(gameTime * 0.05) * 0.3 + 0.7;
+
+ // Outer glow
graphics.setStrokeStyle({
- color: 0xffd700,
- width: 4,
- alpha: corePulse * 0.7,
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 3,
+ alpha: corePulse * 0.4,
});
- const lineLength = 8;
- graphics.moveTo(orbX - lineLength, orbY);
- graphics.lineTo(orbX + lineLength, orbY);
+ graphics.circle(centerX, centerY, 30);
graphics.stroke();
- // Add small decorative dots
- graphics.setFillStyle({ color: 0xffffff, alpha: corePulse * 0.5 });
- graphics.circle(orbX, orbY - 3, 2);
- graphics.circle(orbX, orbY + 3, 2);
+ // Inner core
+ graphics.setFillStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ alpha: corePulse * 0.6
+ });
+ graphics.circle(centerX, centerY, 12);
graphics.fill();
- }
-
- // Enhanced trigram symbol corners with better iconic representation
- const enhancedTrigramPositions = [
- { x: 150, y: 150, symbol: "โฐ", meaning: "Heaven", color: 0xffd700 },
- {
- x: window.innerWidth - 150,
- y: 150,
- symbol: "โฒ",
- meaning: "Fire",
- color: 0xff4500,
- },
- {
- x: 150,
- y: window.innerHeight - 150,
- symbol: "โท",
- meaning: "Earth",
- color: 0x654321,
- },
- {
- x: window.innerWidth - 150,
- y: window.innerHeight - 150,
- symbol: "โต",
- meaning: "Water",
- color: 0x4169e1,
- },
- ];
- enhancedTrigramPositions.forEach(({ x, y, color }, index) => {
- const symbolPulse =
- Math.sin(gameTime * 0.02 + (index * Math.PI) / 2) * 0.3 + 0.7;
-
- // Enhanced trigram symbol background with iconic design
- graphics.setFillStyle({ color: 0x000000, alpha: 0.8 });
- graphics.circle(x, y, 35);
+ // Central dot (meditation point)
+ graphics.setFillStyle({
+ color: DARK_TRIGRAM_THEME.WHITE,
+ alpha: corePulse * 0.8,
+ });
+ graphics.circle(centerX, centerY, 4);
graphics.fill();
- graphics.setStrokeStyle({ color, width: 3, alpha: symbolPulse });
- graphics.circle(x, y, 35);
- graphics.stroke();
+ // Dynamic vital point indicators
+ vitalPoints.forEach((point, index) => {
+ const pointPulse = Math.sin(gameTime * 0.01 + index * 0.5) * 0.3 + 0.7;
+ const distFromCenter = Math.sqrt(
+ Math.pow(point.x - centerX, 2) + Math.pow(point.y - centerY, 2)
+ );
+
+ // Only show points outside the main combat area
+ if (distFromCenter < radius - 50) return;
+
+ // Vital point marker
+ graphics.setFillStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ alpha: pointPulse * 0.4,
+ });
+ graphics.circle(point.x, point.y, 3 + pointPulse * 2);
+ graphics.fill();
- // Inner decorative ring
- graphics.setStrokeStyle({
- color: 0xffd700,
- width: 1,
- alpha: symbolPulse * 0.6,
+ // Vital point ring
+ graphics.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ width: 1,
+ alpha: pointPulse * 0.3,
+ });
+ graphics.circle(point.x, point.y, 8 + pointPulse * 3);
+ graphics.stroke();
});
- graphics.circle(x, y, 25);
- graphics.stroke();
- // Enhanced energy wisps with more iconic movement
- for (let j = 0; j < 4; j++) {
- const wispAngle = gameTime * 0.01 + j * ((Math.PI * 2) / 4);
- const wispRadius = 45 + Math.sin(gameTime * 0.015 + j) * 8;
- const wispX = x + Math.cos(wispAngle) * wispRadius;
- const wispY = y + Math.sin(wispAngle) * wispRadius;
-
- graphics.setFillStyle({ color, alpha: symbolPulse * 0.4 });
- graphics.circle(wispX, wispY, 3);
+ // Traditional Korean dojang boundary markers
+ const trigramPositions: readonly TrigramPosition[] = [
+ { x: window.innerWidth * 0.15, y: window.innerHeight * 0.2, symbol: "โฐ", name: "Heaven" },
+ { x: window.innerWidth * 0.85, y: window.innerHeight * 0.2, symbol: "โฑ", name: "Lake" },
+ { x: window.innerWidth * 0.15, y: window.innerHeight * 0.8, symbol: "โฒ", name: "Fire" },
+ { x: window.innerWidth * 0.85, y: window.innerHeight * 0.8, symbol: "โณ", name: "Thunder" },
+ ] as const;
+
+ trigramPositions.forEach((position, index) => {
+ const trigramPulse = Math.sin(gameTime * 0.008 + index * 1.5) * 0.2 + 0.8;
+
+ // Trigram symbol background
+ graphics.setFillStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ alpha: trigramPulse * 0.1,
+ });
+ graphics.circle(position.x, position.y, 25);
graphics.fill();
- }
- });
-
- // Enhanced atmospheric grid with better visual hierarchy
- graphics.setStrokeStyle({
- color: 0x333333,
- width: 1,
- alpha: 0.1 + Math.sin(gameTime * 0.005) * 0.05,
- });
- const gridSize = 60;
- for (let x = 0; x < window.innerWidth; x += gridSize) {
- const lineAlpha = 0.15 + Math.sin(gameTime * 0.003 + x * 0.01) * 0.08;
- graphics.setStrokeStyle({ color: 0x444444, width: 1, alpha: lineAlpha });
- graphics.moveTo(x, 0);
- graphics.lineTo(x, window.innerHeight);
- graphics.stroke();
- }
- for (let y = 0; y < window.innerHeight; y += gridSize) {
- const lineAlpha = 0.15 + Math.sin(gameTime * 0.003 + y * 0.01) * 0.08;
- graphics.setStrokeStyle({ color: 0x444444, width: 1, alpha: lineAlpha });
- graphics.moveTo(0, y);
- graphics.lineTo(window.innerWidth, y);
- graphics.stroke();
- }
- // Enhanced floating energy motes with iconic shapes
- for (let i = 0; i < 20; i++) {
- const moteX =
- (Math.sin(gameTime * 0.002 + i * 0.8) * window.innerWidth) / 2.5 +
- window.innerWidth / 2;
- const moteY =
- (Math.cos(gameTime * 0.003 + i * 1.2) * window.innerHeight) / 2.5 +
- window.innerHeight / 2;
- const moteAlpha = Math.sin(gameTime * 0.02 + i) * 0.2 + 0.2;
- const moteSize = 2 + Math.sin(gameTime * 0.025 + i * 0.4) * 1;
-
- // Create diamond-shaped energy motes
- graphics.setFillStyle({ color: 0xffd700, alpha: moteAlpha });
- graphics.moveTo(moteX, moteY - moteSize);
- graphics.lineTo(moteX + moteSize, moteY);
- graphics.lineTo(moteX, moteY + moteSize);
- graphics.lineTo(moteX - moteSize, moteY);
- graphics.lineTo(moteX, moteY - moteSize);
- graphics.fill();
- }
- };
+ // Trigram symbol border
+ graphics.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: trigramPulse * 0.3,
+ });
+ graphics.circle(position.x, position.y, 25);
+ graphics.stroke();
+ });
- return ;
+ // Corner accent lines for dojang atmosphere
+ graphics.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: 0.3
+ });
+
+ // Top corners
+ graphics.moveTo(0, 0);
+ graphics.lineTo(50, 0);
+ graphics.moveTo(0, 0);
+ graphics.lineTo(0, 50);
+ graphics.moveTo(window.innerWidth, 0);
+ graphics.lineTo(window.innerWidth - 50, 0);
+ graphics.moveTo(window.innerWidth, 0);
+ graphics.lineTo(window.innerWidth, 50);
+
+ // Bottom corners
+ graphics.moveTo(0, window.innerHeight);
+ graphics.lineTo(50, window.innerHeight);
+ graphics.moveTo(0, window.innerHeight);
+ graphics.lineTo(0, window.innerHeight - 50);
+ graphics.moveTo(window.innerWidth, window.innerHeight);
+ graphics.lineTo(window.innerWidth - 50, window.innerHeight);
+ graphics.moveTo(window.innerWidth, window.innerHeight);
+ graphics.lineTo(window.innerWidth, window.innerHeight - 50);
+ graphics.stroke();
+ },
+ [gameTime, vitalPoints]
+ );
+
+ return (
+
+
+
+ {/* Dark Trigram logo in bottom right corner */}
+ {logoTexture && (
+
+ )}
+
+ );
}
diff --git a/src/components/game/GameUI.tsx b/src/components/game/GameUI.tsx
index df66187c66d..42a9e998d25 100644
--- a/src/components/game/GameUI.tsx
+++ b/src/components/game/GameUI.tsx
@@ -1,5 +1,7 @@
+import { useState, useEffect } from "react";
import type { JSX } from "react";
import type { Graphics as PixiGraphics } from "pixi.js";
+import { useTexture } from "../../hooks/useTexture";
interface GameState {
readonly player1Health: number;
@@ -25,16 +27,16 @@ const GAME_CONFIG = {
ARENA_HEIGHT: 600,
} as const;
-const UI_THEME = {
- TRADITIONAL_RED: 0x8b0000,
- TRADITIONAL_GOLD: 0xffd700,
- TRADITIONAL_BLACK: 0x000000,
- TRADITIONAL_WHITE: 0xffffff,
- KOREAN_GRAY: 0x666666,
- ACCENT_BLUE: 0x4a90e2,
- ENERGY_PURPLE: 0x9370db,
- HEALTH_GREEN: 0x4caf50,
- DANGER_RED: 0xff4444,
+const DARK_TRIGRAM_UI_THEME = {
+ PRIMARY_CYAN: 0x00ffd0,
+ DARK_BG: 0x0a0e12,
+ DARKER_BG: 0x181c20,
+ MEDIUM_BG: 0x23272b,
+ VITAL_ORANGE: 0xff4400,
+ CRITICAL_RED: 0xff3030,
+ WHITE: 0xffffff,
+ GRAY: 0x666666,
+ HEALTH_CYAN: 0x00ffd0,
WARNING_ORANGE: 0xff9800,
} as const;
@@ -106,13 +108,13 @@ function BackgroundOverlay(): JSX.Element {
draw={(g: PixiGraphics) => {
g.clear();
// Traditional Korean pattern background
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_BLACK, alpha: 0.7 });
+ g.setFillStyle({ color: DARK_TRIGRAM_UI_THEME.DARKER_BG, alpha: 0.7 });
g.rect(0, 0, GAME_CONFIG.ARENA_WIDTH, GAME_CONFIG.ARENA_HEIGHT);
g.fill();
// Subtle Korean-inspired border pattern
g.setStrokeStyle({
- color: UI_THEME.TRADITIONAL_RED,
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
width: 2,
alpha: 0.3,
});
@@ -129,63 +131,75 @@ function BackgroundOverlay(): JSX.Element {
}
function GameTitle(): JSX.Element {
+ const { texture: logoTexture } = useTexture("/dark-trigram-256.png");
+
return (
- {/* Enhanced background with iconic elements */}
{
g.clear();
- // Traditional Korean title background
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_BLACK, alpha: 0.9 });
- g.roundRect(-200, -25, 400, 80, 15);
+
+ // Dark background with deeper panel
+ g.setFillStyle({ color: 0x000a12, alpha: 0.95 });
+ g.roundRect(-220, -30, 440, 85, 15);
g.fill();
- // Decorative border with traditional patterns
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_RED, width: 3 });
- g.roundRect(-200, -25, 400, 80, 15);
+ // Cyan glowing border with futuristic style
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 3,
+ });
+ g.roundRect(-220, -30, 440, 85, 15);
g.stroke();
- // Inner golden accent
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_GOLD, width: 1 });
- g.roundRect(-195, -20, 390, 70, 12);
+ // Inner glow with tech accent
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: 0.7,
+ });
+ g.roundRect(-210, -20, 420, 65, 12);
g.stroke();
- // Corner decorative elements (icon-like)
- const corners: Array<[number, number]> = [
- [-190, -15],
- [190, -15],
- [-190, 45],
- [190, 45],
- ];
- corners.forEach(([x, y]) => {
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_GOLD, alpha: 0.8 });
- g.circle(x, y, 4);
- g.fill();
+ // Add tech-looking diagonal slashes
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: 0.4,
});
+ g.moveTo(-220, 0);
+ g.lineTo(-190, -30);
+ g.moveTo(220, 0);
+ g.lineTo(190, -30);
+ g.stroke();
}}
/>
- {/* Enhanced title with better typography */}
-
+ {/* Use the game logo sprite instead of text */}
+ {logoTexture && (
+
+ )}
+
@@ -208,90 +222,98 @@ function HealthBar({
return (
- {/* Enhanced health bar frame with icons */}
{
g.clear();
- // Main frame with gradient-like effect
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_BLACK, alpha: 0.95 });
+ // Dark frame
+ g.setFillStyle({ color: DARK_TRIGRAM_UI_THEME.DARK_BG, alpha: 0.95 });
g.roundRect(-10, -10, 240, 80, 12);
g.fill();
- // Traditional border
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_RED, width: 3 });
+ // Cyan border
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 3,
+ });
g.roundRect(-10, -10, 240, 80, 12);
g.stroke();
- // Inner accent border
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_GOLD, width: 1 });
+ // Inner glow
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: 0.7,
+ });
g.roundRect(-5, -5, 230, 70, 8);
g.stroke();
}}
/>
- {/* Player label with icon */}
- {/* Enhanced health bar with better visual feedback */}
{
g.clear();
- // Background with subtle texture
- g.setFillStyle({ color: 0x2a2a2a });
+ // Dark background
+ g.setFillStyle({ color: DARK_TRIGRAM_UI_THEME.MEDIUM_BG });
g.roundRect(10, 0, 200, 22, 11);
g.fill();
// Health fill with dynamic color
const healthColor =
health > 60
- ? UI_THEME.HEALTH_GREEN
+ ? DARK_TRIGRAM_UI_THEME.HEALTH_CYAN
: health > 30
- ? UI_THEME.WARNING_ORANGE
- : UI_THEME.DANGER_RED;
+ ? DARK_TRIGRAM_UI_THEME.WARNING_ORANGE
+ : DARK_TRIGRAM_UI_THEME.CRITICAL_RED;
g.setFillStyle({ color: healthColor });
g.roundRect(12, 2, health * 1.96, 18, 9);
g.fill();
- // Health bar shine effect
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_WHITE, alpha: 0.3 });
- g.roundRect(12, 2, health * 1.96, 8, 4);
- g.fill();
-
- // Border with player-specific color
- const borderColor = isPlayerOne
- ? UI_THEME.ACCENT_BLUE
- : UI_THEME.TRADITIONAL_RED;
- g.setStrokeStyle({ color: borderColor, width: 2 });
+ // Cyan glow border
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 2,
+ });
g.roundRect(10, 0, 200, 22, 11);
g.stroke();
}}
/>
- {/* Health percentage text */}
@@ -309,19 +331,21 @@ function Timer({ gameState, gameTime }: TimerProps): JSX.Element {
return (
- {/* Enhanced timer background */}
{
g.clear();
const bgColor = isUrgent
- ? UI_THEME.DANGER_RED
- : UI_THEME.TRADITIONAL_BLACK;
+ ? DARK_TRIGRAM_UI_THEME.CRITICAL_RED
+ : DARK_TRIGRAM_UI_THEME.DARK_BG;
g.setFillStyle({ color: bgColor, alpha: 0.9 });
g.roundRect(-80, -20, 160, 40, 12);
g.fill();
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_GOLD, width: 2 });
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 2,
+ });
g.roundRect(-80, -20, 160, 40, 12);
g.stroke();
}}
@@ -332,13 +356,17 @@ function Timer({ gameState, gameTime }: TimerProps): JSX.Element {
anchor={{ x: 0.5, y: 0.5 }}
alpha={pulse}
style={{
- fontFamily: "Noto Sans KR",
+ fontFamily: "Orbitron, Noto Sans KR",
fontSize: isUrgent ? 22 : 20,
fill: isUrgent
- ? UI_THEME.TRADITIONAL_WHITE
- : UI_THEME.TRADITIONAL_GOLD,
+ ? DARK_TRIGRAM_UI_THEME.WHITE
+ : DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
fontWeight: "bold",
- stroke: { color: UI_THEME.TRADITIONAL_BLACK, width: 2 },
+ dropShadow: {
+ color: 0x00ffd0,
+ blur: 6,
+ distance: 0,
+ },
}}
/>
@@ -374,48 +402,48 @@ function ControlLegend({ gameStarted }: { gameStarted: boolean }): JSX.Element {
return (
- {/* Enhanced control panel */}
{
g.clear();
- // Main panel background
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_BLACK, alpha: 0.95 });
+ // Dark panel background
+ g.setFillStyle({ color: DARK_TRIGRAM_UI_THEME.DARK_BG, alpha: 0.95 });
g.roundRect(0, 0, 380, 140, 15);
g.fill();
- // Traditional Korean border
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_RED, width: 3 });
+ // Cyan border
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 3,
+ });
g.roundRect(0, 0, 380, 140, 15);
g.stroke();
- // Inner decorative border
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_GOLD, width: 1 });
- g.roundRect(5, 5, 370, 130, 12);
- g.stroke();
-
- // Section dividers
+ // Inner glow
g.setStrokeStyle({
- color: UI_THEME.KOREAN_GRAY,
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
width: 1,
- alpha: 0.5,
+ alpha: 0.7,
});
- g.moveTo(20, 35);
- g.lineTo(360, 35);
+ g.roundRect(5, 5, 370, 130, 12);
g.stroke();
}}
/>
- {/* Enhanced control labels with icons */}
@@ -424,9 +452,14 @@ function ControlLegend({ gameStarted }: { gameStarted: boolean }): JSX.Element {
x={20}
y={45}
style={{
- fontFamily: "Noto Sans KR",
+ fontFamily: "Orbitron, Noto Sans KR",
fontSize: 13,
- fill: UI_THEME.TRADITIONAL_WHITE,
+ fill: DARK_TRIGRAM_UI_THEME.WHITE,
+ dropShadow: {
+ color: 0x00ffd0,
+ blur: 2,
+ distance: 0,
+ },
}}
/>
@@ -435,9 +468,14 @@ function ControlLegend({ gameStarted }: { gameStarted: boolean }): JSX.Element {
x={20}
y={65}
style={{
- fontFamily: "Noto Sans KR",
+ fontFamily: "Orbitron, Noto Sans KR",
fontSize: 13,
- fill: UI_THEME.TRADITIONAL_WHITE,
+ fill: DARK_TRIGRAM_UI_THEME.WHITE,
+ dropShadow: {
+ color: 0x00ffd0,
+ blur: 2,
+ distance: 0,
+ },
}}
/>
@@ -446,9 +484,14 @@ function ControlLegend({ gameStarted }: { gameStarted: boolean }): JSX.Element {
x={20}
y={85}
style={{
- fontFamily: "Noto Sans KR",
+ fontFamily: "Orbitron, Noto Sans KR",
fontSize: 13,
- fill: UI_THEME.TRADITIONAL_WHITE,
+ fill: DARK_TRIGRAM_UI_THEME.WHITE,
+ dropShadow: {
+ color: 0x00ffd0,
+ blur: 2,
+ distance: 0,
+ },
}}
/>
@@ -459,7 +502,7 @@ function ControlLegend({ gameStarted }: { gameStarted: boolean }): JSX.Element {
style={{
fontFamily: "Noto Sans KR",
fontSize: 12,
- fill: UI_THEME.WARNING_ORANGE,
+ fill: DARK_TRIGRAM_UI_THEME.WARNING_ORANGE,
fontStyle: "italic",
}}
/>
@@ -485,12 +528,15 @@ function TrigramDecorations(): JSX.Element {
{
g.clear();
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_BLACK, alpha: 0.8 });
+ g.setFillStyle({
+ color: DARK_TRIGRAM_UI_THEME.DARKER_BG,
+ alpha: 0.8,
+ });
g.rect(0, 0, window.innerWidth, 50);
g.fill();
g.setStrokeStyle({
- color: UI_THEME.TRADITIONAL_RED,
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
width: 2,
alpha: 0.6,
});
@@ -506,11 +552,17 @@ function TrigramDecorations(): JSX.Element {
{
g.clear();
- g.setFillStyle({ color: UI_THEME.TRADITIONAL_RED, alpha: 0.3 });
+ g.setFillStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ alpha: 0.3,
+ });
g.circle(0, 0, 20);
g.fill();
- g.setStrokeStyle({ color: UI_THEME.TRADITIONAL_GOLD, width: 1 });
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_UI_THEME.PRIMARY_CYAN,
+ width: 1,
+ });
g.circle(0, 0, 20);
g.stroke();
}}
@@ -522,7 +574,7 @@ function TrigramDecorations(): JSX.Element {
style={{
fontFamily: "serif",
fontSize: 14,
- fill: UI_THEME.TRADITIONAL_WHITE,
+ fill: DARK_TRIGRAM_UI_THEME.WHITE,
}}
/>
@@ -540,6 +592,8 @@ function StartButton({
onStartMatch,
gameTime,
}: StartButtonProps): JSX.Element {
+ const [isHovered, setIsHovered] = useState(false);
+
return (
setIsHovered(true)}
+ onPointerLeave={() => setIsHovered(false)}
>
{
g.clear();
const pulse = Math.sin(gameTime * 0.1) * 0.3 + 0.7;
- g.setFillStyle({ color: 0x8b0000, alpha: pulse });
- g.roundRect(-120, -40, 240, 80, 15);
+
+ // Cyberpunk-style button with deeper background
+ g.setFillStyle({ color: 0x000a12, alpha: 0.95 });
+ g.roundRect(-140, -50, 280, 100, 15);
g.fill();
- g.setStrokeStyle({ color: 0xffffff, width: 3 });
- g.roundRect(-120, -40, 240, 80, 15);
+
+ // Dynamic border
+ g.setStrokeStyle({
+ color: isHovered ? 0x00ffd0 : 0x005566,
+ width: isHovered ? 3 : 2,
+ alpha: pulse,
+ });
+ g.roundRect(-140, -50, 280, 100, 15);
g.stroke();
+
+ // Inner tech accent
+ g.setStrokeStyle({
+ color: 0x00ffd0,
+ width: 1,
+ alpha: isHovered ? 0.7 : 0.4,
+ });
+ g.roundRect(-130, -40, 260, 80, 12);
+ g.stroke();
+
+ // Tech accent triangles on corners
+ g.setFillStyle({ color: 0x00ffd0, alpha: isHovered ? 0.7 : 0.4 });
+ g.moveTo(-140, -30);
+ g.lineTo(-120, -50);
+ g.lineTo(-140, -50);
+ g.lineTo(-140, -30);
+ g.fill();
+
+ g.moveTo(140, -30);
+ g.lineTo(120, -50);
+ g.lineTo(140, -50);
+ g.lineTo(140, -30);
+ g.fill();
+
+ g.moveTo(-140, 30);
+ g.lineTo(-120, 50);
+ g.lineTo(-140, 50);
+ g.lineTo(-140, 30);
+ g.fill();
+
+ g.moveTo(140, 30);
+ g.lineTo(120, 50);
+ g.lineTo(140, 50);
+ g.lineTo(140, 30);
+ g.fill();
+
+ // Add pulsing glow effect when hovered
+ if (isHovered) {
+ g.setStrokeStyle({
+ color: 0x00ffd0,
+ width: 2,
+ alpha: pulse * 0.5,
+ });
+ g.roundRect(-150, -60, 300, 120, 20);
+ g.stroke();
+ }
}}
/>
@@ -593,6 +712,18 @@ function VictoryScreen({
winner,
onResetMatch,
}: VictoryScreenProps): JSX.Element {
+ const [isHovered, setIsHovered] = useState(false);
+ const [time, setTime] = useState(0);
+ const { texture: logoTexture } = useTexture("/dark-trigram-256.png");
+
+ // Add animation timer
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTime((t: number) => t + 0.016);
+ }, 16);
+ return () => clearInterval(interval);
+ }, []);
+
return (
+ {/* Victory logo */}
+ {logoTexture && (
+
+ )}
+
setIsHovered(true)}
+ onPointerLeave={() => setIsHovered(false)}
>
{
g.clear();
- g.setFillStyle({ color: 0x666666 });
- g.roundRect(-60, -20, 120, 40, 8);
+ const pulse = Math.sin(time * 0.1) * 0.3 + 0.7;
+
+ // Cyberpunk button
+ g.setFillStyle({ color: 0x000a12, alpha: 0.95 });
+ g.roundRect(-80, -25, 160, 50, 10);
g.fill();
- g.setStrokeStyle({ color: 0xffffff, width: 2 });
- g.roundRect(-60, -20, 120, 40, 8);
+
+ g.setStrokeStyle({
+ color: isHovered ? 0x00ffd0 : 0x005566,
+ width: isHovered ? 3 : 2,
+ alpha: pulse,
+ });
+ g.roundRect(-80, -25, 160, 50, 10);
g.stroke();
+
+ // Tech accent corners
+ g.setFillStyle({ color: 0x00ffd0, alpha: isHovered ? 0.6 : 0.3 });
+ g.moveTo(-80, -15);
+ g.lineTo(-70, -25);
+ g.lineTo(-80, -25);
+ g.lineTo(-80, -15);
+ g.fill();
+
+ g.moveTo(80, -15);
+ g.lineTo(70, -25);
+ g.lineTo(80, -25);
+ g.lineTo(80, -15);
+ g.fill();
+
+ if (isHovered) {
+ g.setStrokeStyle({
+ color: 0x00ffd0,
+ width: 2,
+ alpha: pulse * 0.5,
+ });
+ g.roundRect(-90, -35, 180, 70, 15);
+ g.stroke();
+ }
}}
/>
diff --git a/src/components/game/HitEffectsLayer.tsx b/src/components/game/HitEffectsLayer.tsx
index 950c44f3618..a18fa750731 100644
--- a/src/components/game/HitEffectsLayer.tsx
+++ b/src/components/game/HitEffectsLayer.tsx
@@ -1,4 +1,18 @@
+import { useState, useEffect, useCallback } from "react";
import type { JSX } from "react";
+import type { Graphics as PixiGraphics } from "pixi.js";
+import { useTexture } from "../../hooks/useTexture";
+
+// Dark Trigram theme constants
+const DARK_TRIGRAM_THEME = {
+ PRIMARY_CYAN: 0x00ffd0,
+ VITAL_ORANGE: 0xff4400,
+ CRITICAL_RED: 0xff3030,
+ WHITE: 0xffffff,
+ DARK_BG: 0x0a0e12,
+ ENERGY_BLUE: 0x4169e1,
+ LIGHTNING_YELLOW: 0xffd700,
+} as const;
export interface HitEffect {
readonly id: string;
@@ -8,51 +22,382 @@ export interface HitEffect {
readonly technique: string;
readonly life: number;
readonly maxLife: number;
+ readonly isVitalHit?: boolean;
+ readonly trigram?: string;
}
interface HitEffectsLayerProps {
readonly hitEffects: readonly HitEffect[];
}
+interface Particle {
+ readonly id: string;
+ readonly x: number;
+ readonly y: number;
+ readonly vx: number;
+ readonly vy: number;
+ readonly life: number;
+ readonly maxLife: number;
+ readonly color: number;
+ readonly size: number;
+}
+
export function HitEffectsLayer({
hitEffects,
}: HitEffectsLayerProps): JSX.Element {
+ const [particles, setParticles] = useState([]);
+ const [screenShake, setScreenShake] = useState({ x: 0, y: 0, intensity: 0 });
+ const { texture: logoTexture } = useTexture("/dark-trigram-256.png");
+
+ // Generate explosive particles for vital hits
+ const generateHitParticles = useCallback((effect: HitEffect): Particle[] => {
+ const particleCount = effect.isVitalHit ? 25 : 12;
+ const particles: Particle[] = [];
+
+ for (let i = 0; i < particleCount; i++) {
+ const angle = (Math.PI * 2 * i) / particleCount;
+ const speed = effect.isVitalHit
+ ? 3 + Math.random() * 2
+ : 1.5 + Math.random();
+ const life = 60 + Math.random() * 30;
+
+ particles.push({
+ id: `particle_${effect.id}_${i}`,
+ x: effect.x,
+ y: effect.y,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed,
+ life,
+ maxLife: life,
+ color: effect.isVitalHit
+ ? DARK_TRIGRAM_THEME.VITAL_ORANGE
+ : DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ size: effect.isVitalHit ? 4 : 2,
+ });
+ }
+
+ return particles;
+ }, []);
+
+ // Generate screen shake for powerful hits
+ const generateScreenShake = useCallback((effect: HitEffect): void => {
+ const intensity = effect.isVitalHit ? 15 : Math.min(effect.damage / 2, 8);
+ setScreenShake({
+ x: (Math.random() - 0.5) * intensity,
+ y: (Math.random() - 0.5) * intensity,
+ intensity,
+ });
+ }, []);
+
+ // Handle new hit effects
+ useEffect(() => {
+ hitEffects.forEach((effect) => {
+ // Generate particles for new effects
+ if (effect.life === effect.maxLife) {
+ const newParticles = generateHitParticles(effect);
+ setParticles((prev) => [...prev, ...newParticles]);
+ generateScreenShake(effect);
+ }
+ });
+ }, [hitEffects, generateHitParticles, generateScreenShake]);
+
+ // Update particles and screen shake
+ useEffect(() => {
+ const interval = setInterval(() => {
+ // Update particles
+ setParticles((prev) =>
+ prev
+ .map((particle) => ({
+ ...particle,
+ x: particle.x + particle.vx,
+ y: particle.y + particle.vy,
+ vx: particle.vx * 0.98, // Air resistance
+ vy: particle.vy * 0.98 + 0.1, // Gravity
+ life: particle.life - 1,
+ }))
+ .filter((particle) => particle.life > 0)
+ );
+
+ // Decay screen shake
+ setScreenShake((prev) => ({
+ x: prev.x * 0.9,
+ y: prev.y * 0.9,
+ intensity: prev.intensity * 0.9,
+ }));
+ }, 16); // ~60fps
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const drawParticles = useCallback(
+ (g: PixiGraphics) => {
+ g.clear();
+
+ particles.forEach((particle) => {
+ const alpha = particle.life / particle.maxLife;
+ const size = particle.size * alpha;
+
+ // Energy particles with glow effect
+ g.setFillStyle({ color: particle.color, alpha: alpha * 0.8 });
+ g.circle(particle.x, particle.y, size);
+ g.fill();
+
+ // Outer glow
+ g.setFillStyle({ color: particle.color, alpha: alpha * 0.3 });
+ g.circle(particle.x, particle.y, size * 2);
+ g.fill();
+ });
+ },
+ [particles]
+ );
+
return (
- <>
+
+ {/* Particle effects */}
+
+
+ {/* Enhanced hit effects with Korean aesthetics */}
{hitEffects.map((effect) => {
const alpha = effect.life / effect.maxLife;
- const scale = 1 + (1 - alpha) * 0.5;
+ const scale = 1 + (1 - alpha) * 0.8;
+ const isVitalHit = effect.damage > 25;
+
return (
-
-
+ {/* Background explosion ring for vital hits */}
+ {isVitalHit && (
+ {
+ g.clear();
+ const ringSize = (1 - alpha) * 100;
+
+ // Outer explosion ring
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ width: 3,
+ alpha: alpha * 0.6,
+ });
+ g.circle(0, 0, ringSize);
+ g.stroke();
+
+ // Inner energy ring
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.LIGHTNING_YELLOW,
+ width: 1,
+ alpha: alpha * 0.8,
+ });
+ g.circle(0, 0, ringSize * 0.7);
+ g.stroke();
+
+ // Lightning effect for vital hits
+ if (alpha > 0.7) {
+ for (let i = 0; i < 8; i++) {
+ const angle = (Math.PI * 2 * i) / 8;
+ const length = 30 + Math.random() * 20;
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.LIGHTNING_YELLOW,
+ width: 2,
+ alpha: alpha * 0.5,
+ });
+ g.moveTo(0, 0);
+ g.lineTo(
+ Math.cos(angle) * length + (Math.random() - 0.5) * 10,
+ Math.sin(angle) * length + (Math.random() - 0.5) * 10
+ );
+ g.stroke();
+ }
+ }
+ }}
+ />
+ )}
+
+ {/* Dark Trigram logo for critical hits */}
+ {isVitalHit && logoTexture && alpha > 0.5 && (
+
+ )}
+
+ {/* Enhanced damage number with Korean style */}
+
+ {
+ g.clear();
+ // Background panel for damage number
+ g.setFillStyle({
+ color: DARK_TRIGRAM_THEME.DARK_BG,
+ alpha: alpha * 0.8,
+ });
+ g.roundRect(-40, -15, 80, 30, 8);
+ g.fill();
+
+ // Border with trigram colors
+ g.setStrokeStyle({
+ color: isVitalHit
+ ? DARK_TRIGRAM_THEME.VITAL_ORANGE
+ : DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 2,
+ alpha: alpha,
+ });
+ g.roundRect(-40, -15, 80, 30, 8);
+ g.stroke();
+ }}
+ />
+
+
+
+
+ {/* Vital hit indicator with Korean text and effects */}
+ {isVitalHit && (
+
+ {
+ g.clear();
+ // Energy burst background
+ g.setFillStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ alpha: alpha * 0.3,
+ });
+ g.roundRect(-60, -20, 120, 40, 12);
+ g.fill();
+
+ // Pulsing border
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.WHITE,
+ width: 2,
+ alpha: alpha * 0.9,
+ });
+ g.roundRect(-60, -20, 120, 40, 12);
+ g.stroke();
+ }}
+ />
+
+
+
+ )}
+
+ {/* Technique name with enhanced Korean typography */}
+
+ {
+ g.clear();
+ // Traditional Korean panel background
+ g.setFillStyle({ color: 0x000000, alpha: alpha * 0.7 });
+ g.roundRect(-80, -12, 160, 24, 6);
+ g.fill();
+
+ // Traditional red border
+ g.setStrokeStyle({
+ color: 0x8b0000,
+ width: 1,
+ alpha: alpha * 0.8,
+ });
+ g.roundRect(-80, -12, 160, 24, 6);
+ g.stroke();
+ }}
+ />
+
+
+
+
+ {/* Trigram symbol for technique classification */}
+ {effect.trigram && (
+
+ {
+ g.clear();
+ // Circular background for trigram
+ g.setFillStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ alpha: alpha * 0.2,
+ });
+ g.circle(0, 0, 25);
+ g.fill();
+
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 2,
+ alpha: alpha * 0.6,
+ });
+ g.circle(0, 0, 25);
+ g.stroke();
+ }}
+ />
+
+
+
+ )}
);
})}
- >
+
);
}
diff --git a/src/components/game/Player.tsx b/src/components/game/Player.tsx
index 98a1a552515..0f1bb45d066 100644
--- a/src/components/game/Player.tsx
+++ b/src/components/game/Player.tsx
@@ -338,7 +338,7 @@ export function PlayerContainer({
// Enhanced attack execution with audio feedback
const executeAttack = useCallback(
- (stance: TrigramStance) => {
+ (stance: TrigramStance): void => {
if (!gameStarted || attackCooldown > 0) return;
const technique = TRIGRAM_TECHNIQUES[stance];
@@ -346,9 +346,14 @@ export function PlayerContainer({
setPlayerState((prev) => PlayerStateManager.executeAttack(prev, stance));
- // Play attack sound based on damage
+ // Play attack sound based on damage with vital point detection
+ const isVitalPoint = technique.damage > 30;
audio.playAttackSound(technique.damage);
+ if (isVitalPoint) {
+ audio.playHitSound(technique.damage, true);
+ }
+
const attackX =
playerState.facing === "right"
? playerState.x + technique.range
@@ -377,7 +382,7 @@ export function PlayerContainer({
playerState.x,
playerState.y,
onAttack,
- audio, // Added audio dependency
+ audio,
]
);
@@ -420,7 +425,7 @@ export function PlayerContainer({
// Enhanced AI behavior with better decision making
const handleAI = useCallback(
- (delta: number) => {
+ (delta: number): void => {
if (!opponentPosition || !gameStarted || isPlayerOne) return;
const distance = AISystem.calculateDistance(
@@ -463,7 +468,7 @@ export function PlayerContainer({
// Enhanced player input handling
const handlePlayerInput = useCallback(
- (delta: number) => {
+ (delta: number): void => {
if (!isPlayerOne) {
handleAI(delta);
return;
@@ -480,7 +485,7 @@ export function PlayerContainer({
? GAME_BALANCE.BASE_MOVE_SPEED * GAME_BALANCE.BLOCKING_SPEED_MULTIPLIER
: GAME_BALANCE.BASE_MOVE_SPEED;
- // Movement controls
+ // Movement controls using delta for frame-rate independence
if (keys.has("KeyA") || keys.has("ArrowLeft")) {
newX -= moveSpeed * delta;
newFacing = "left";
@@ -546,19 +551,19 @@ export function PlayerContainer({
// Enhanced game loop with better performance
useTick(
useCallback(
- (ticker: { deltaTime: number }) => {
+ (ticker: { deltaTime: number }): void => {
const delta = ticker.deltaTime;
setAnimationTime((prev) => prev + delta);
if (!gameStarted) return;
- // Update cooldowns
+ // Update cooldowns using delta time
setAttackCooldown((prev) => Math.max(0, prev - delta));
- // Handle input
+ // Handle input with delta timing
handlePlayerInput(delta);
- // Update stamina
+ // Update stamina with delta timing
setPlayerState((prev) => PlayerStateManager.updateStamina(prev, delta));
},
[gameStarted, handlePlayerInput]
diff --git a/src/components/game/PlayerVisuals.tsx b/src/components/game/PlayerVisuals.tsx
index ed27bce4773..21b38c8680a 100644
--- a/src/components/game/PlayerVisuals.tsx
+++ b/src/components/game/PlayerVisuals.tsx
@@ -1,47 +1,41 @@
import type { JSX } from "react";
import type { Graphics as PixiGraphics } from "pixi.js";
-import type { PlayerState, TrigramStance } from "./Player";
-
-// Enhanced visual theme with more iconic elements
-const VISUAL_THEME = {
- PLAYER_1_COLOR: 0x4a90e2,
- PLAYER_2_COLOR: 0x8b0000,
- TRADITIONAL_GOLD: 0xffd700,
- KOREAN_WHITE: 0xffffff,
- COMBAT_ICONS: {
- HEALTH: "โค๏ธ",
- STAMINA: "โก",
- ATTACK: "โ๏ธ",
- DEFENSE: "๐ก๏ธ",
- PRECISION: "๐ฏ",
- },
- ENERGY_COLORS: {
- geon: 0xffd700, // Gold - Heaven (Lightning energy) ๐ฅ
- tae: 0x87ceeb, // Sky Blue - Lake (Water energy) ๐
- li: 0xff4500, // Red Orange - Fire (Flame energy) โก
- jin: 0x9370db, // Purple - Thunder (Electric energy) ๐ฅ
- son: 0x98fb98, // Pale Green - Wind (Air energy) ๐ช๏ธ
- gam: 0x4169e1, // Royal Blue - Water (Liquid energy) ๐ก๏ธ
- gan: 0x8b4513, // Saddle Brown - Mountain (Earth energy) ๐ฟ
- gon: 0x654321, // Dark Brown - Earth (Stone energy) ๐ค
- },
+import { PlayerState, TrigramStance } from "./Player";
+
+// Dark Trigram theme constants
+const DARK_TRIGRAM_THEME = {
+ PRIMARY_CYAN: 0x00ffd0,
+ DARK_BG: 0x0a0e12,
+ DARKER_BG: 0x181c20,
+ MEDIUM_BG: 0x23272b,
+ VITAL_ORANGE: 0xff4400,
+ CRITICAL_RED: 0xff3030,
+ WHITE: 0xffffff,
+ HEALTH_CYAN: 0x00ffd0,
+ WARNING_ORANGE: 0xff9800,
STANCE_ICONS: {
- geon: "๐ฅ", // Heaven - Power strikes
- tae: "๐", // Lake - Flowing combos
- li: "โก", // Fire - Fast attacks
- jin: "๐ฅ", // Thunder - Explosive bursts
- son: "๐ช๏ธ", // Wind - Continuous pressure
- gam: "๐ก๏ธ", // Water - Evasion and counters
- gan: "๐ฟ", // Mountain - Immovable defense
- gon: "๐ค", // Earth - Throws and takedowns
- },
+ geon: "๐ฅ", // Heaven
+ tae: "๐", // Lake
+ li: "โก", // Fire
+ jin: "๐ฅ", // Thunder
+ son: "๐ช๏ธ", // Wind
+ gam: "๐ก๏ธ", // Water
+ gan: "๐ฟ", // Mountain
+ gon: "๐ค", // Earth
+ } as Record,
} as const;
-// Helper functions for stance color and symbol
-export function getStanceColor(stance: TrigramStance): number {
- return VISUAL_THEME.ENERGY_COLORS[stance];
+// Trigram technique interface
+interface TrigramTechnique {
+ readonly name: string;
+ readonly damage: number;
+ readonly stamina: number;
+ readonly speed: number;
+ readonly range: number;
+ readonly vitalPoints: readonly string[];
}
+// Export the helper function for testing
export function getTrigramSymbol(stance: TrigramStance): string {
const symbols: Record = {
geon: "โฐ", // Heaven
@@ -56,20 +50,33 @@ export function getTrigramSymbol(stance: TrigramStance): string {
return symbols[stance];
}
-interface TrigramTechnique {
- readonly name: string;
- readonly damage: number;
- readonly stamina: number;
- readonly speed: number;
- readonly range: number;
- readonly vitalPoints: readonly string[];
+// Export stance color function for testing
+export function getStanceColor(stance: TrigramStance): number {
+ const stanceColors: Record = {
+ geon: 0xffd700, // Gold - Heaven
+ tae: 0x87ceeb, // Sky Blue - Lake
+ li: 0xff4500, // Red Orange - Fire
+ jin: 0x9370db, // Purple - Thunder
+ son: 0x98fb98, // Pale Green - Wind
+ gam: 0x4169e1, // Royal Blue - Water
+ gan: 0x8b4513, // Saddle Brown - Mountain
+ gon: 0x654321, // Dark Brown - Earth
+ };
+ return stanceColors[stance];
+}
+
+// Helper function to get trigram symbols
+function getTrigramSymbolInternal(stance: TrigramStance): string {
+ return getTrigramSymbol(stance);
}
+// Main PlayerVisuals component
interface PlayerVisualsProps {
readonly playerState: PlayerState;
readonly currentTechnique: TrigramTechnique;
readonly isPlayerOne: boolean;
readonly animationTime: number;
+ readonly showImpactEffect?: boolean;
}
export function PlayerVisuals({
@@ -77,16 +84,20 @@ export function PlayerVisuals({
currentTechnique,
isPlayerOne,
animationTime,
+ showImpactEffect = false,
}: PlayerVisualsProps): JSX.Element {
return (
- <>
+
+
+
+
{playerState.isAttacking && (
)}
+
+ {playerState.isAttacking && showImpactEffect && (
+ 30}
+ />
+ )}
+
{playerState.isMoving && (
)}
- >
+
);
}
@@ -110,202 +131,143 @@ function PlayerBody({
isPlayerOne,
animationTime,
}: {
- playerState: PlayerState;
- isPlayerOne: boolean;
- animationTime: number;
+ readonly playerState: PlayerState;
+ readonly isPlayerOne: boolean;
+ readonly animationTime: number;
}): JSX.Element {
+ const auraPulse = Math.sin(animationTime * 0.1) * 0.3 + 0.7;
+
+ // Use isPlayerOne for player-specific styling
+ const playerColor = isPlayerOne
+ ? DARK_TRIGRAM_THEME.PRIMARY_CYAN
+ : DARK_TRIGRAM_THEME.VITAL_ORANGE;
+
return (
- <>
- {/* Enhanced fighter body with martial arts uniform */}
- {
- g.clear();
+ {
+ g.clear();
- const bodyColor = isPlayerOne
- ? VISUAL_THEME.PLAYER_1_COLOR
- : VISUAL_THEME.PLAYER_2_COLOR;
+ // Dark silhouette body with player-specific accent
+ g.setFillStyle({ color: DARK_TRIGRAM_THEME.DARKER_BG, alpha: 0.95 });
+ g.rect(-25, -90, 50, 90);
+ g.fill();
- // Traditional Korean martial arts uniform (dobok) with better design
- g.setFillStyle({ color: 0xffffff, alpha: 0.9 }); // White dobok
- g.rect(-25, -90, 50, 90);
- g.fill();
+ // Player-specific glowing outline
+ g.setStrokeStyle({ color: playerColor, width: 4, alpha: auraPulse });
+ g.rect(-25, -90, 50, 90);
+ g.stroke();
- // Traditional belt with rank colors
- const beltColor = isPlayerOne ? 0x000080 : 0x8b0000; // Blue vs Red belt
- g.setFillStyle({ color: beltColor });
- g.rect(-27, -25, 54, 10);
- g.fill();
+ // Enhanced head with player-specific glow
+ g.setFillStyle({ color: DARK_TRIGRAM_THEME.MEDIUM_BG });
+ g.circle(0, -75, 15);
+ g.fill();
+
+ g.setStrokeStyle({
+ color: playerColor,
+ width: 3,
+ alpha: auraPulse * 0.8,
+ });
+ g.circle(0, -75, 15);
+ g.stroke();
- // Arm positioning for martial arts stance
- g.setStrokeStyle({ color: bodyColor, width: 8 });
- if (playerState.facing === "right") {
- // Right guard position
- g.moveTo(-15, -60);
- g.lineTo(-35, -45);
- g.moveTo(15, -60);
- g.lineTo(40, -50);
- } else {
- // Left guard position
- g.moveTo(15, -60);
- g.lineTo(35, -45);
- g.moveTo(-15, -60);
- g.lineTo(-40, -50);
- }
+ // Arm positioning with player-specific glow
+ g.setStrokeStyle({ color: playerColor, width: 6, alpha: 0.8 });
+ if (playerState.facing === "right") {
+ // Right-facing stance
+ g.moveTo(25, -60);
+ g.lineTo(45, -40);
+ g.moveTo(-25, -60);
+ g.lineTo(-15, -50);
+ } else {
+ // Left-facing stance
+ g.moveTo(-25, -60);
+ g.lineTo(-45, -40);
+ g.moveTo(25, -60);
+ g.lineTo(15, -50);
+ }
+ g.stroke();
+
+ // Pulsing aura when attacking or high stamina
+ if (playerState.isAttacking || playerState.stamina > 95) {
+ const attackAlpha = playerState.isAttacking ? 0.8 : 0.4;
+ g.setStrokeStyle({
+ color: playerColor,
+ width: 8,
+ alpha: attackAlpha * auraPulse,
+ });
+ g.circle(0, -45, 55 + Math.sin(animationTime * 0.3) * 10);
g.stroke();
+ }
- // Head with martial arts headband
- g.setFillStyle({ color: 0xffdbac }); // Skin tone
- g.circle(0, -75, 15);
- g.fill();
+ // Enhanced blocking effect with player distinction
+ if (playerState.isBlocking) {
+ g.setStrokeStyle({
+ color: 0xffffff,
+ width: 4,
+ alpha: 0.9,
+ });
+ g.circle(0, -45, 35);
+ g.stroke();
- // Traditional headband
- g.setFillStyle({ color: beltColor });
- g.rect(-18, -85, 36, 6);
+ g.setFillStyle({ color: 0xffffff, alpha: 0.3 });
+ g.circle(0, -45, 30);
g.fill();
-
- // Enhanced stance aura with particle-like effects
- if (playerState.isAttacking) {
- const attackPulse = Math.sin(animationTime * 0.3) * 0.4 + 0.6;
- const stanceColor = VISUAL_THEME.ENERGY_COLORS[playerState.stance];
-
- // Energy aura around fighter
- g.setStrokeStyle({
- color: stanceColor,
- width: 8,
- alpha: attackPulse * 0.8,
- });
- g.circle(0, -45, 45 + Math.sin(animationTime * 0.5) * 5);
- g.stroke();
-
- // Inner energy core
- g.setFillStyle({
- color: stanceColor,
- alpha: attackPulse * 0.3,
- });
- g.circle(0, -45, 35);
- g.fill();
-
- // Energy sparks
- for (let i = 0; i < 8; i++) {
- const angle = (i / 8) * Math.PI * 2 + animationTime * 0.1;
- const sparkX =
- Math.cos(angle) * (50 + Math.sin(animationTime + i) * 10);
- const sparkY =
- -45 + Math.sin(angle) * (50 + Math.sin(animationTime + i) * 10);
-
- g.setFillStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- alpha: attackPulse * 0.8,
- });
- g.circle(sparkX, sparkY, 2 + Math.sin(animationTime * 2 + i) * 1);
- g.fill();
- }
- }
-
- // Enhanced blocking effect with traditional patterns
- if (playerState.isBlocking) {
- // Main blocking barrier
- g.setStrokeStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- width: 6,
- alpha: 0.9,
- });
- g.rect(-30, -95, 60, 100);
- g.stroke();
-
- // Traditional Korean geometric patterns
- g.setStrokeStyle({
- color: VISUAL_THEME.TRADITIONAL_GOLD,
- width: 2,
- alpha: 0.7,
- });
-
- // Cross pattern
- g.moveTo(-30, -45);
- g.lineTo(30, -45);
- g.moveTo(0, -95);
- g.lineTo(0, 5);
- g.stroke();
-
- // Corner decorations with proper destructuring
- const corners: readonly [number, number][] = [
- [-25, -90],
- [25, -90],
- [-25, 0],
- [25, 0],
- ];
- corners.forEach(([x, y]) => {
- g.setFillStyle({
- color: VISUAL_THEME.TRADITIONAL_GOLD,
- alpha: 0.6,
- });
- g.rect(x - 3, y - 3, 6, 6);
- g.fill();
- });
- }
- }}
- />
- >
+ }
+ }}
+ />
);
}
-function StanceIndicator({ stance }: { stance: TrigramStance }): JSX.Element {
+// Update StanceIndicator to use the internal function
+function StanceIndicator({
+ stance,
+}: {
+ readonly stance: TrigramStance;
+}): JSX.Element {
+ const pulse = Math.sin(Date.now() * 0.002) * 0.2 + 0.8;
+
return (
-
- {/* Enhanced stance display with combat icons */}
+
+ {/* Stance symbol background */}
{
g.clear();
-
- const stanceColor = VISUAL_THEME.ENERGY_COLORS[stance];
-
- // Main background with enhanced design
- g.setFillStyle({ color: 0x000000, alpha: 0.95 });
- g.roundRect(-55, -125, 110, 35, 15);
+ g.setFillStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ alpha: pulse * 0.3,
+ });
+ g.circle(0, 0, 25);
g.fill();
- // Glowing border with stance-specific color
- g.setStrokeStyle({ color: stanceColor, width: 3, alpha: 0.9 });
- g.roundRect(-55, -125, 110, 35, 15);
- g.stroke();
-
- // Inner glow effect
g.setStrokeStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- width: 1,
- alpha: 0.7,
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 2,
+ alpha: pulse,
});
- g.roundRect(-50, -120, 100, 25, 12);
+ g.circle(0, 0, 25);
g.stroke();
-
- // Corner energy markers
- const corners: readonly [number, number][] = [
- [-45, -115],
- [45, -115],
- [-45, -95],
- [45, -95],
- ];
- corners.forEach(([x, y]) => {
- g.setFillStyle({ color: stanceColor, alpha: 0.8 });
- g.circle(x, y, 3);
- g.fill();
- });
}}
/>
- {/* Combat style icon with trigram symbol */}
+ {/* Trigram symbol */}
+
+ {/* Stance icon */}
+
@@ -315,82 +277,72 @@ function StanceIndicator({ stance }: { stance: TrigramStance }): JSX.Element {
function HealthStaminaBars({
playerState,
}: {
- playerState: PlayerState;
+ readonly playerState: PlayerState;
}): JSX.Element {
+ const healthColor =
+ playerState.health > 50
+ ? DARK_TRIGRAM_THEME.PRIMARY_CYAN
+ : playerState.health > 25
+ ? DARK_TRIGRAM_THEME.WARNING_ORANGE
+ : DARK_TRIGRAM_THEME.CRITICAL_RED;
+
return (
- <>
- {/* Enhanced health bar with icon */}
-
+
+ {/* Health bar */}
{
g.clear();
// Background
- g.setFillStyle({ color: 0x2a2a2a });
- g.roundRect(-35, -135, 70, 10, 5);
+ g.setFillStyle({ color: DARK_TRIGRAM_THEME.DARKER_BG, alpha: 0.8 });
+ g.rect(-30, 0, 60, 8);
g.fill();
- // Health fill with dynamic color
- const healthColor =
- playerState.health > 50
- ? 0x4caf50
- : playerState.health > 25
- ? 0xff9800
- : 0xff4444;
+ // Health fill
g.setFillStyle({ color: healthColor });
- g.roundRect(-33, -133, playerState.health * 0.66, 6, 3);
+ g.rect(-30, 0, (playerState.health / 100) * 60, 8);
g.fill();
// Border
- g.setStrokeStyle({ color: 0xffffff, width: 1 });
- g.roundRect(-35, -135, 70, 10, 5);
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 1,
+ });
+ g.rect(-30, 0, 60, 8);
g.stroke();
}}
/>
- {/* Enhanced stamina bar with icon */}
-
+ {/* Stamina bar */}
{
g.clear();
- // Health bar background
- g.setFillStyle({ color: 0x2a2a2a });
- g.roundRect(-35, -142, 70, 8, 4);
+ // Background
+ g.setFillStyle({ color: DARK_TRIGRAM_THEME.DARKER_BG, alpha: 0.8 });
+ g.rect(-30, 0, 60, 6);
g.fill();
// Stamina fill
- g.setFillStyle({ color: 0xffc107 });
- g.roundRect(-33, -118, playerState.stamina * 0.66, 4, 2);
+ g.setFillStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ alpha: 0.8,
+ });
+ g.rect(-30, 0, (playerState.stamina / 100) * 60, 6);
g.fill();
// Border
- g.setStrokeStyle({ color: 0xffffff, width: 1 });
- g.roundRect(-35, -120, 70, 8, 4);
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.PRIMARY_CYAN,
+ width: 1,
+ alpha: 0.6,
+ });
+ g.rect(-30, 0, 60, 6);
g.stroke();
}}
/>
- >
+
);
}
@@ -399,94 +351,100 @@ function AttackEffect({
technique,
animationTime,
}: {
- playerState: PlayerState;
- technique: TrigramTechnique;
- animationTime: number;
+ readonly playerState: PlayerState;
+ readonly technique: TrigramTechnique;
+ readonly animationTime: number;
}): JSX.Element {
+ const isVital = technique.damage > 30;
+ const burstColor = isVital
+ ? DARK_TRIGRAM_THEME.VITAL_ORANGE
+ : DARK_TRIGRAM_THEME.PRIMARY_CYAN;
+ const burstAlpha = Math.sin(animationTime * 0.3) * 0.4 + 0.6;
+
return (
- <>
- {/* Main attack effect */}
- {
- g.clear();
- const attackAlpha = Math.sin(animationTime * 0.3) * 0.4 + 0.6;
- const attackWidth = technique.range;
- const attackHeight = 30 + technique.damage * 0.8;
- const stanceColor = VISUAL_THEME.ENERGY_COLORS[playerState.stance];
-
- // Enhanced attack effect with energy burst
- g.setFillStyle({ color: stanceColor, alpha: attackAlpha * 0.6 });
- g.ellipse(attackWidth / 2, 0, attackWidth, attackHeight);
- g.fill();
+ {
+ g.clear();
- // Attack core with pulsing effect
- g.setFillStyle({ color: 0xffffff, alpha: attackAlpha });
- g.ellipse(attackWidth / 2, 0, attackWidth * 0.3, attackHeight * 0.5);
- g.fill();
- }}
- />
+ const attackX = playerState.facing === "right" ? 35 : -35;
- {/* Attack impact indicator */}
-
- >
+ // Main attack burst
+ g.setFillStyle({ color: burstColor, alpha: burstAlpha * 0.6 });
+ g.circle(attackX, -45, 20 + Math.sin(animationTime * 0.4) * 8);
+ g.fill();
+
+ // Attack ring
+ g.setStrokeStyle({ color: burstColor, width: 3, alpha: burstAlpha });
+ g.circle(attackX, -45, 25 + Math.sin(animationTime * 0.5) * 5);
+ g.stroke();
+
+ // Technique-specific effect
+ if (isVital) {
+ // Vital point targeting crosshair
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ width: 2,
+ alpha: burstAlpha,
+ });
+ g.moveTo(attackX - 15, -45);
+ g.lineTo(attackX + 15, -45);
+ g.moveTo(attackX, -60);
+ g.lineTo(attackX, -30);
+ g.stroke();
+ }
+ }}
+ />
);
}
-function AttackImpactEffect({
+// Export AttackImpactEffect for use in other components
+export function AttackImpactEffect({
technique,
playerState,
animationTime,
+ isVital = false,
}: {
- technique: TrigramTechnique;
- playerState: PlayerState;
- animationTime: number;
+ readonly technique: TrigramTechnique;
+ readonly playerState: PlayerState;
+ readonly animationTime: number;
+ readonly isVital?: boolean;
}): JSX.Element {
const impactX =
playerState.facing === "right"
? 35 + technique.range
: -35 - technique.range;
+ const impactColor = isVital
+ ? DARK_TRIGRAM_THEME.VITAL_ORANGE
+ : DARK_TRIGRAM_THEME.PRIMARY_CYAN;
+ const impactAlpha = Math.sin(animationTime * 0.4) * 0.5 + 0.5;
return (
{
g.clear();
- const impactAlpha = Math.sin(animationTime * 0.4) * 0.5 + 0.5;
- const stanceColor = VISUAL_THEME.ENERGY_COLORS[playerState.stance];
- // Impact explosion effect
- g.setFillStyle({
- color: stanceColor,
- alpha: impactAlpha * 0.7,
- });
- g.circle(impactX, 0, 15 + Math.sin(animationTime * 0.6) * 5);
+ // Impact burst
+ g.setFillStyle({ color: impactColor, alpha: impactAlpha * 0.8 });
+ g.circle(impactX, -45, 15 + Math.sin(animationTime * 0.6) * 5);
g.fill();
- // Impact ring
- g.setStrokeStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- width: 3,
- alpha: impactAlpha,
- });
- g.circle(impactX, 0, 20 + Math.sin(animationTime * 0.8) * 8);
+ // Impact shockwave
+ g.setStrokeStyle({ color: impactColor, width: 2, alpha: impactAlpha });
+ g.circle(impactX, -45, 20 + Math.sin(animationTime * 0.4) * 10);
g.stroke();
- // Impact sparks
- for (let i = 0; i < 8; i++) {
- const angle = (i / 8) * Math.PI * 2;
- const sparkDistance = 25 + Math.sin(animationTime * 0.7 + i) * 10;
- const sparkX = impactX + Math.cos(angle) * sparkDistance;
- const sparkY = Math.sin(angle) * sparkDistance;
-
- g.setFillStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- alpha: impactAlpha * 0.8,
+ if (isVital) {
+ // Vital hit indicator
+ g.setStrokeStyle({
+ color: DARK_TRIGRAM_THEME.VITAL_ORANGE,
+ width: 3,
+ alpha: impactAlpha,
});
- g.circle(sparkX, sparkY, 2);
- g.fill();
+ g.moveTo(impactX - 20, -45);
+ g.lineTo(impactX + 20, -45);
+ g.moveTo(impactX, -65);
+ g.lineTo(impactX, -25);
+ g.stroke();
}
}}
/>
@@ -495,48 +453,35 @@ function AttackImpactEffect({
function MovementTrail({
isPlayerOne,
+ animationTime,
facing,
}: {
- isPlayerOne: boolean;
- animationTime: number;
- facing: "left" | "right";
+ readonly isPlayerOne: boolean;
+ readonly animationTime: number;
+ readonly facing: "left" | "right";
}): JSX.Element {
+ // Use isPlayerOne for different trail colors
+ const trailColor = isPlayerOne
+ ? DARK_TRIGRAM_THEME.PRIMARY_CYAN
+ : DARK_TRIGRAM_THEME.VITAL_ORANGE;
+
return (
{
g.clear();
- const trailAlpha = 0.4;
- const trailColor = isPlayerOne
- ? VISUAL_THEME.PLAYER_1_COLOR
- : VISUAL_THEME.PLAYER_2_COLOR;
- // Enhanced movement trail with speed lines
+ const trailX = facing === "right" ? -20 : 20;
+ const trailAlpha = Math.sin(animationTime * 0.2) * 0.3 + 0.4;
+
+ // Movement trail effect
for (let i = 0; i < 5; i++) {
- const offset = facing === "right" ? -(i * 8) : i * 8;
- const alpha = trailAlpha * (1 - i * 0.15);
+ const offsetX = trailX + i * (facing === "right" ? -8 : 8);
+ const alpha = trailAlpha * (1 - i * 0.2);
- g.setFillStyle({
- color: trailColor,
- alpha: alpha,
- });
- g.rect(offset - 20, -75, 40, 70);
+ g.setFillStyle({ color: trailColor, alpha });
+ g.circle(offsetX, -45, 8 - i * 1.5);
g.fill();
}
-
- // Speed lines for dynamic movement
- g.setStrokeStyle({
- color: VISUAL_THEME.KOREAN_WHITE,
- width: 2,
- alpha: trailAlpha * 0.6,
- });
-
- for (let i = 0; i < 6; i++) {
- const lineY = -60 + i * 15;
- const lineOffset = facing === "right" ? -30 : 30;
- g.moveTo(lineOffset, lineY);
- g.lineTo(lineOffset + (facing === "right" ? -15 : 15), lineY);
- g.stroke();
- }
}}
/>
);
diff --git a/src/hooks/useTexture.ts b/src/hooks/useTexture.ts
new file mode 100644
index 00000000000..78e159cec61
--- /dev/null
+++ b/src/hooks/useTexture.ts
@@ -0,0 +1,90 @@
+import { useState, useEffect } from "react";
+import { Texture, Assets } from "pixi.js";
+
+interface UseTextureResult {
+ readonly texture: Texture | null;
+ readonly loading: boolean;
+ readonly error: string | null;
+}
+
+// Texture cache to prevent reloading
+const textureCache = new Map();
+
+export function useTexture(url: string): UseTextureResult {
+ const [texture, setTexture] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ // Check cache first
+ if (textureCache.has(url)) {
+ const cachedTexture = textureCache.get(url);
+ if (cachedTexture) {
+ setTexture(cachedTexture);
+ setLoading(false);
+ return;
+ }
+ }
+
+ let cancelled = false;
+
+ const loadTexture = async (): Promise => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const loadedTexture = await Assets.load(url);
+
+ if (!cancelled) {
+ // Cache the texture
+ textureCache.set(url, loadedTexture);
+ setTexture(loadedTexture);
+ setLoading(false);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : `Failed to load texture: ${url}`;
+ setError(errorMessage);
+ setLoading(false);
+ console.warn(`[useTexture] ${errorMessage}`);
+ }
+ }
+ };
+
+ loadTexture();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [url]);
+
+ return { texture, loading, error };
+}
+
+// Preload commonly used textures
+export async function preloadGameTextures(): Promise {
+ const commonTextures = [
+ "/dark-trigram-256.png",
+ "/trigram-effects.png",
+ "/vital-points.png",
+ ];
+
+ try {
+ await Promise.all(
+ commonTextures.map(async (url) => {
+ if (!textureCache.has(url)) {
+ const texture = await Assets.load(url);
+ textureCache.set(url, texture);
+ }
+ })
+ );
+ } catch (error) {
+ console.warn(
+ "[preloadGameTextures] Some textures failed to preload:",
+ error
+ );
+ }
+}
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index df828227e6c..2afe5e43994 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/audio/AudioManager.ts","./src/audio/AudioUtils.ts","./src/audio/DefaultSoundGenerator.ts","./src/audio/placeholder-sounds.ts","./src/audio/__tests__/AudioManager.integration.test.ts","./src/audio/__tests__/AudioManager.test.ts","./src/audio/__tests__/AudioUtils.test.ts","./src/audio/__tests__/integration.test.tsx","./src/components/__tests__/AudioIntegration.test.tsx","./src/components/__tests__/GameAudio.test.tsx","./src/components/game/DojangBackground.test.tsx","./src/components/game/DojangBackground.tsx","./src/components/game/GameEngine.test.tsx","./src/components/game/GameEngine.tsx","./src/components/game/GameUI.test.tsx","./src/components/game/GameUI.tsx","./src/components/game/HitEffectsLayer.test.tsx","./src/components/game/HitEffectsLayer.tsx","./src/components/game/Player.test.tsx","./src/components/game/Player.tsx","./src/components/game/PlayerVisuals.test.tsx","./src/components/game/PlayerVisuals.tsx","./src/test/setup.ts","./src/types/pixi-react.d.ts"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/audio/AudioManager.ts","./src/audio/AudioUtils.ts","./src/audio/DefaultSoundGenerator.ts","./src/audio/placeholder-sounds.ts","./src/audio/__tests__/AudioManager.integration.test.ts","./src/audio/__tests__/AudioManager.test.ts","./src/audio/__tests__/AudioUtils.test.ts","./src/audio/__tests__/integration.test.tsx","./src/components/__tests__/AudioIntegration.test.tsx","./src/components/__tests__/GameAudio.test.tsx","./src/components/game/DojangBackground.test.tsx","./src/components/game/DojangBackground.tsx","./src/components/game/GameEngine.test.tsx","./src/components/game/GameEngine.tsx","./src/components/game/GameUI.test.tsx","./src/components/game/GameUI.tsx","./src/components/game/HitEffectsLayer.test.tsx","./src/components/game/HitEffectsLayer.tsx","./src/components/game/InputSystem.tsx","./src/components/game/Player.test.tsx","./src/components/game/Player.tsx","./src/components/game/PlayerVisuals.test.tsx","./src/components/game/PlayerVisuals.tsx","./src/components/ui/CyberpunkButton.tsx","./src/components/ui/CyberpunkPanel.tsx","./src/hooks/useTexture.ts","./src/test/setup.ts","./src/types/pixi-react.d.ts"],"version":"5.8.3"}
\ No newline at end of file