Compare commits
10 Commits
961dcc3a25
...
cad05ad895
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad05ad895 | ||
|
|
cfdad79207 | ||
|
|
326b3036b6 | ||
|
|
c0ced95d46 | ||
|
|
ac5b83afee | ||
|
|
c09bc6edd5 | ||
|
|
4236d0098d | ||
|
|
2533e3c50e | ||
|
|
344569edee | ||
|
|
abe1e2ca54 |
42
CLAUDE.md
42
CLAUDE.md
@ -25,7 +25,7 @@ The application loads scripts in a specific order due to class dependencies:
|
|||||||
|
|
||||||
1. **Utilities**: `vector.js`, `physics.js` (foundational math/physics)
|
1. **Utilities**: `vector.js`, `physics.js` (foundational math/physics)
|
||||||
2. **Entities**: `player.js`, `puck.js` (game objects)
|
2. **Entities**: `player.js`, `puck.js` (game objects)
|
||||||
3. **Systems**: `renderer.js`, `physics-system.js`, `ai-system.js`, `rules-system.js`
|
3. **Systems**: `renderer.js`, `physics-system.js`, `ai-system.js`, `rules-system.js`, `debug-system.js`
|
||||||
4. **Engine**: `game-state.js`, `game-engine.js`, `main.js` (orchestration)
|
4. **Engine**: `game-state.js`, `game-engine.js`, `main.js` (orchestration)
|
||||||
|
|
||||||
### Key Classes and Responsibilities
|
### Key Classes and Responsibilities
|
||||||
@ -35,9 +35,10 @@ The application loads scripts in a specific order due to class dependencies:
|
|||||||
- **GameState** (`game-state.js`): Score tracking, time management, penalty system
|
- **GameState** (`game-state.js`): Score tracking, time management, penalty system
|
||||||
- **Player** (`player.js`): Hockey player with AI, physics, and role-based behavior
|
- **Player** (`player.js`): Hockey player with AI, physics, and role-based behavior
|
||||||
- **Puck** (`puck.js`): Physics-based puck with collision detection
|
- **Puck** (`puck.js`): Physics-based puck with collision detection
|
||||||
- **Renderer** (`renderer.js`): 2D canvas rendering with camera system
|
- **Renderer** (`renderer.js`): 2D canvas rendering with camera system and debug visualizations
|
||||||
- **AISystem** (`ai-system.js`): Team formations and strategic AI behaviors
|
- **AISystem** (`ai-system.js`): Team formations and strategic AI behaviors
|
||||||
- **RulesSystem** (`rules-system.js`): Hockey rule enforcement (offside, icing, penalties)
|
- **RulesSystem** (`rules-system.js`): Hockey rule enforcement (offside, icing, penalties)
|
||||||
|
- **DebugSystem** (`debug-system.js`): Comprehensive debugging interface with real-time inspection
|
||||||
|
|
||||||
### Game Loop Architecture
|
### Game Loop Architecture
|
||||||
The engine runs at 60 FPS using `requestAnimationFrame` with these phases:
|
The engine runs at 60 FPS using `requestAnimationFrame` with these phases:
|
||||||
@ -55,10 +56,45 @@ The engine runs at 60 FPS using `requestAnimationFrame` with these phases:
|
|||||||
|
|
||||||
## Controls Reference
|
## Controls Reference
|
||||||
- `SPACE` - Pause/Resume game
|
- `SPACE` - Pause/Resume game
|
||||||
- `D` - Toggle debug information
|
- `D` - Toggle debug information and visual overlays
|
||||||
- `R` - Reset game
|
- `R` - Reset game
|
||||||
- `F11` - Toggle fullscreen
|
- `F11` - Toggle fullscreen
|
||||||
- `Mouse Wheel` - Camera zoom
|
- `Mouse Wheel` - Camera zoom
|
||||||
|
- **Debug Mode Button** - Open/close comprehensive debug panel
|
||||||
|
|
||||||
|
## Debug System Features
|
||||||
|
|
||||||
|
### Enhanced Debug Panel
|
||||||
|
When debug mode is active, you can pause the game and inspect all variables:
|
||||||
|
|
||||||
|
- **Real-time Debug Panel**: Right-side panel with live game state information
|
||||||
|
- **Player State Inspection**: Click any player to see detailed attributes, AI state, and physics data
|
||||||
|
- **Game State Monitoring**: Period, time, score, pause status, faceoff information
|
||||||
|
- **Puck Tracking**: Position, velocity, ownership, and physics data
|
||||||
|
|
||||||
|
### Interactive Debug Features
|
||||||
|
- **Player Selection**: Click players in debug panel or directly on the canvas
|
||||||
|
- **Target Visualization**: Selected players show bright cyan lines to their target positions
|
||||||
|
- **Visual Feedback**: Hover effects, selection highlighting, and smooth animations
|
||||||
|
|
||||||
|
### Console Debug Functions
|
||||||
|
Access via `debugHelpers` global object:
|
||||||
|
- `debugHelpers.getPlayers()` - Get all player objects
|
||||||
|
- `debugHelpers.getHomeTeam()` / `debugHelpers.getAwayTeam()` - Get team players
|
||||||
|
- `debugHelpers.getPlayer(id)` - Get specific player by ID
|
||||||
|
- `debugHelpers.getPuck()` - Get puck object with full state
|
||||||
|
- `debugHelpers.getGameState()` - Get complete game state
|
||||||
|
- `debugHelpers.getPuckCarrier()` - Get player currently carrying the puck
|
||||||
|
- `debugHelpers.exportGameState()` - Export full game state for analysis
|
||||||
|
|
||||||
|
### Debug Visualizations
|
||||||
|
When visual debug mode is active (`D` key):
|
||||||
|
- **Velocity Vectors**: Red arrows showing player movement direction
|
||||||
|
- **Target Lines**: Green dashed lines to player destinations
|
||||||
|
- **AI Targets**: Yellow dotted lines to AI decision targets
|
||||||
|
- **Energy Bars**: Visual energy indicators above players
|
||||||
|
- **Puck Carrier Highlight**: Yellow dashed circle around puck carrier
|
||||||
|
- **Selected Player**: Cyan line to target with crosshair marker
|
||||||
|
|
||||||
## File Structure Notes
|
## File Structure Notes
|
||||||
- All code is vanilla JavaScript (ES6 classes)
|
- All code is vanilla JavaScript (ES6 classes)
|
||||||
|
|||||||
103
PLAYER.md
Normal file
103
PLAYER.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# Player Logic Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The `Player` class represents hockey players with AI-driven behavior, physics simulation, and role-based positioning. Each player has attributes, states, and intelligent decision-making capabilities.
|
||||||
|
|
||||||
|
## Player Structure
|
||||||
|
|
||||||
|
### Core Properties
|
||||||
|
- **Identity**: `id`, `name`, `team` ('home' or 'away'), `role` (C, LW, RW, LD, RD, G)
|
||||||
|
- **Physics**: `position`, `velocity`, `targetPosition`, `mass`, `radius`, `maxSpeed`
|
||||||
|
- **Attributes**: Speed, shooting, passing, defense, checking, puck handling, awareness (70-90 range)
|
||||||
|
- **State**: Energy, puck possession, checking status, injury status
|
||||||
|
- **AI State**: Current target, behavior mode, reaction timing
|
||||||
|
|
||||||
|
### Player Roles
|
||||||
|
- **C (Center)**: Primary playmaker, takes faceoffs
|
||||||
|
- **LW/RW (Wings)**: Offensive forwards, positioned on left/right sides
|
||||||
|
- **LD/RD (Defense)**: Defensive players, protect own zone
|
||||||
|
- **G (Goalie)**: Larger, slower, stays in crease area
|
||||||
|
|
||||||
|
## AI Behavior System
|
||||||
|
|
||||||
|
### Main Update Loop
|
||||||
|
1. **Energy Management**: Drains based on movement speed, regenerates when stationary
|
||||||
|
2. **Movement Physics**: Accelerates toward target position with friction
|
||||||
|
3. **Angle Updates**: Smoothly rotates toward target angle
|
||||||
|
4. **Behavior Selection**: Chooses actions based on game situation
|
||||||
|
|
||||||
|
### Faceoff Handling
|
||||||
|
When `gameState.faceoff.isActive` is true:
|
||||||
|
- Players move to specific faceoff positions based on their role
|
||||||
|
- Centers line up at the dot, wings/defense stay outside the circle
|
||||||
|
- Goalies remain in their nets
|
||||||
|
|
||||||
|
### Behavior States
|
||||||
|
|
||||||
|
#### With Puck
|
||||||
|
- **Shooting**: Attempts shots when in good scoring position (<250 units from goal)
|
||||||
|
- **Passing**: Finds open teammates when under pressure (<60 units from opponent)
|
||||||
|
- **Advancing**: Moves toward enemy goal while avoiding opponents
|
||||||
|
|
||||||
|
#### Without Puck
|
||||||
|
- **Chasing**: Closest non-goalie teammate pursues loose puck
|
||||||
|
- **Checking**: Applies body checks to opponents with puck
|
||||||
|
- **Defending**: Positions between puck carrier and own goal
|
||||||
|
- **Formation**: Moves to tactical position based on game situation
|
||||||
|
|
||||||
|
### Formation System
|
||||||
|
Players dynamically switch between attacking and defensive formations:
|
||||||
|
|
||||||
|
**Attacking Formation** (when team has puck or puck in offensive zone):
|
||||||
|
- Forwards push toward opponent's goal
|
||||||
|
- Defense provides support from behind
|
||||||
|
- Creates offensive pressure
|
||||||
|
|
||||||
|
**Defensive Formation** (when opponent has puck):
|
||||||
|
- Players fall back toward own goal
|
||||||
|
- Tight defensive positioning
|
||||||
|
- Focus on puck recovery
|
||||||
|
|
||||||
|
## Key AI Methods
|
||||||
|
|
||||||
|
### Decision Making
|
||||||
|
- `updateAI()`: Main AI decision loop with reaction timing
|
||||||
|
- `behaviorWithPuck()`: Offensive actions (shoot, pass, advance)
|
||||||
|
- `behaviorWithoutPuck()`: Defensive/support actions
|
||||||
|
- `determineTeamState()`: Analyzes if team is attacking or defending
|
||||||
|
|
||||||
|
### Movement & Positioning
|
||||||
|
- `getFormationPosition()`: Calculates tactical position based on game state
|
||||||
|
- `moveToPosition()`: Sets movement target and facing direction
|
||||||
|
- `findBestPathToGoal()`: Intelligent pathfinding around opponents
|
||||||
|
|
||||||
|
### Interactions
|
||||||
|
- `shoot()`: Fires puck toward target with accuracy based on attributes
|
||||||
|
- `pass()`: Passes to teammate with appropriate power
|
||||||
|
- `checkPlayer()`: Physical body check that affects opponent's velocity
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
- `isClosestPlayerToPuck()`: Determines if this player should chase loose puck
|
||||||
|
- `hasGoodShootingAngle()`: Checks for clear shot opportunity
|
||||||
|
- `findBestPassTarget()`: Locates optimal passing target
|
||||||
|
|
||||||
|
## Goalie Behavior
|
||||||
|
Goalies have specialized behavior:
|
||||||
|
- Stay within crease boundaries
|
||||||
|
- Position based on puck location
|
||||||
|
- Move to intercept shots
|
||||||
|
- Don't participate in offensive plays
|
||||||
|
|
||||||
|
## Physics Integration
|
||||||
|
- Collision detection with radius-based boundaries
|
||||||
|
- Realistic acceleration and deceleration
|
||||||
|
- Energy affects maximum speed (tired players move slower)
|
||||||
|
- Friction applied for realistic movement
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
- Team-colored circles with role labels
|
||||||
|
- Yellow indicator when player has puck
|
||||||
|
- Rotation shows facing direction
|
||||||
|
- Debug information available
|
||||||
|
|
||||||
|
The player system creates realistic hockey gameplay through intelligent AI, formation tactics, and physics-based movement.
|
||||||
43
README.md
43
README.md
@ -51,7 +51,8 @@ hockey-manager/
|
|||||||
│ │ ├── renderer.js # 2D rendering system
|
│ │ ├── renderer.js # 2D rendering system
|
||||||
│ │ ├── physics-system.js # Physics calculations
|
│ │ ├── physics-system.js # Physics calculations
|
||||||
│ │ ├── ai-system.js # AI formation and strategy
|
│ │ ├── ai-system.js # AI formation and strategy
|
||||||
│ │ └── rules-system.js # Hockey rules enforcement
|
│ │ ├── rules-system.js # Hockey rules enforcement
|
||||||
|
│ │ └── debug-system.js # Comprehensive debugging interface
|
||||||
│ └── utils/
|
│ └── utils/
|
||||||
│ ├── vector.js # 2D vector mathematics
|
│ ├── vector.js # 2D vector mathematics
|
||||||
│ └── physics.js # Physics utility functions
|
│ └── physics.js # Physics utility functions
|
||||||
@ -63,8 +64,9 @@ hockey-manager/
|
|||||||
2. **Start playing**: The game will automatically initialize and start
|
2. **Start playing**: The game will automatically initialize and start
|
||||||
3. **Use controls**:
|
3. **Use controls**:
|
||||||
- **Space**: Pause/Resume
|
- **Space**: Pause/Resume
|
||||||
- **D**: Toggle debug mode
|
- **D**: Toggle debug mode with visual overlays
|
||||||
- **R**: Reset game
|
- **R**: Reset game
|
||||||
|
- **Debug Mode Button**: Open comprehensive debug panel
|
||||||
- **Mouse wheel**: Zoom in/out
|
- **Mouse wheel**: Zoom in/out
|
||||||
- **F11**: Toggle fullscreen
|
- **F11**: Toggle fullscreen
|
||||||
|
|
||||||
@ -84,6 +86,43 @@ hockey-manager/
|
|||||||
- **Play/Pause** - Start/stop game simulation
|
- **Play/Pause** - Start/stop game simulation
|
||||||
- **Speed Control** - Adjust game speed (0.5x, 1x, 2x, 4x)
|
- **Speed Control** - Adjust game speed (0.5x, 1x, 2x, 4x)
|
||||||
- **Reset Game** - Return to initial game state
|
- **Reset Game** - Return to initial game state
|
||||||
|
- **Debug Mode** - Open comprehensive debug panel for game inspection
|
||||||
|
|
||||||
|
## Debug System
|
||||||
|
|
||||||
|
The hockey engine includes a comprehensive debug system for inspecting game state and player behavior:
|
||||||
|
|
||||||
|
### Debug Panel Features
|
||||||
|
- **Real-time Game State**: Live monitoring of period, time, score, and game status
|
||||||
|
- **Player State Inspection**: Click any player to view detailed attributes, AI decisions, and physics data
|
||||||
|
- **Puck Tracking**: Complete puck state including position, velocity, and ownership
|
||||||
|
- **Interactive Selection**: Click players in the panel or directly on the canvas for detailed inspection
|
||||||
|
|
||||||
|
### Visual Debug Overlays
|
||||||
|
When debug mode is active (`D` key):
|
||||||
|
- **Velocity Vectors**: Red arrows showing player movement direction and speed
|
||||||
|
- **Target Lines**: Green dashed lines indicating where players are trying to move
|
||||||
|
- **AI Targets**: Yellow dotted lines showing AI decision targets
|
||||||
|
- **Energy Bars**: Visual indicators of player energy levels
|
||||||
|
- **Selected Player Visualization**: Bright cyan lines showing target destination with crosshair markers
|
||||||
|
|
||||||
|
### Console Debug Functions
|
||||||
|
Access programmatic debugging via the global `debugHelpers` object:
|
||||||
|
```javascript
|
||||||
|
debugHelpers.getPlayers() // All player objects
|
||||||
|
debugHelpers.getHomeTeam() // Home team players only
|
||||||
|
debugHelpers.getAwayTeam() // Away team players only
|
||||||
|
debugHelpers.getPlayer(id) // Specific player by ID
|
||||||
|
debugHelpers.getPuck() // Puck object with full state
|
||||||
|
debugHelpers.getPuckCarrier() // Player currently with puck
|
||||||
|
debugHelpers.exportGameState() // Complete game state export
|
||||||
|
```
|
||||||
|
|
||||||
|
This debug system is essential for:
|
||||||
|
- **AI Development**: Understanding player decision-making and behavior patterns
|
||||||
|
- **Performance Analysis**: Monitoring physics calculations and render performance
|
||||||
|
- **Game Balance**: Analyzing player attributes and gameplay mechanics
|
||||||
|
- **Bug Investigation**: Detailed state inspection for troubleshooting issues
|
||||||
|
|
||||||
## Game Mechanics
|
## Game Mechanics
|
||||||
|
|
||||||
|
|||||||
166
css/styles.css
166
css/styles.css
@ -129,4 +129,170 @@ button:active {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Debug Panel Styles */
|
||||||
|
.debug-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 400px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border: 2px solid #4a90e2;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-header {
|
||||||
|
background: #4a90e2;
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-content {
|
||||||
|
max-height: calc(80vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-section h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #4a90e2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-section h5 {
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-team {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
min-height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player:active {
|
||||||
|
transform: scale(0.98) translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player.selected {
|
||||||
|
background: rgba(74, 144, 226, 0.4);
|
||||||
|
border: 2px solid #4a90e2;
|
||||||
|
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player.selected:hover {
|
||||||
|
background: rgba(74, 144, 226, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-player-info {
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-value {
|
||||||
|
color: #4a90e2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-coords {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#debug-selected-player {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-attribute {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-attribute-name {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-attribute-value {
|
||||||
|
color: #4a90e2;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
38
index.html
38
index.html
@ -26,16 +26,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="game-stats">
|
<div id="game-stats">
|
||||||
<div id="shots">Shots: <span id="home-shots">0</span> - <span id="away-shots">0</span></div>
|
<div id="shots">Shots: <span id="home-shots">0</span> - <span id="away-shots">0</span></div>
|
||||||
<div id="penalties">
|
|
||||||
<div id="home-penalties"></div>
|
|
||||||
<div id="away-penalties"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls">
|
<div id="controls">
|
||||||
<button id="play-pause">Play/Pause</button>
|
<button id="play-pause">Play/Pause</button>
|
||||||
<button id="speed-control">Speed: 1x</button>
|
<button id="speed-control">Speed: 1x</button>
|
||||||
<button id="reset-game">Reset</button>
|
<button id="reset-game">Reset</button>
|
||||||
|
<button id="debug-toggle">Debug Mode</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="debug-panel" class="debug-panel hidden">
|
||||||
|
<div class="debug-header">
|
||||||
|
<h3>Debug Panel</h3>
|
||||||
|
<button id="debug-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="debug-content">
|
||||||
|
<div class="debug-section">
|
||||||
|
<h4>Game State</h4>
|
||||||
|
<div id="debug-game-state"></div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-section">
|
||||||
|
<h4>Puck</h4>
|
||||||
|
<div id="debug-puck"></div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-section">
|
||||||
|
<h4>Players</h4>
|
||||||
|
<div class="debug-team">
|
||||||
|
<h5>Home Team</h5>
|
||||||
|
<div id="debug-home-players"></div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-team">
|
||||||
|
<h5>Away Team</h5>
|
||||||
|
<div id="debug-away-players"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-section">
|
||||||
|
<h4>Selected Player</h4>
|
||||||
|
<div id="debug-selected-player">Click a player to see detailed info</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,6 +76,7 @@
|
|||||||
<script src="src/systems/physics-system.js"></script>
|
<script src="src/systems/physics-system.js"></script>
|
||||||
<script src="src/systems/ai-system.js"></script>
|
<script src="src/systems/ai-system.js"></script>
|
||||||
<script src="src/systems/rules-system.js"></script>
|
<script src="src/systems/rules-system.js"></script>
|
||||||
|
<script src="src/systems/debug-system.js"></script>
|
||||||
<script src="src/engine/game-state.js"></script>
|
<script src="src/engine/game-state.js"></script>
|
||||||
<script src="src/engine/game-engine.js"></script>
|
<script src="src/engine/game-engine.js"></script>
|
||||||
<script src="src/engine/main.js"></script>
|
<script src="src/engine/main.js"></script>
|
||||||
|
|||||||
@ -19,6 +19,10 @@ class GameEngine {
|
|||||||
this.setupPlayers();
|
this.setupPlayers();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.setupControls();
|
this.setupControls();
|
||||||
|
|
||||||
|
// Initialize debug system after all other systems are set up
|
||||||
|
window.debugMode = false; // Initialize debug mode
|
||||||
|
this.debugSystem = new DebugSystem(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPlayers() {
|
setupPlayers() {
|
||||||
@ -90,13 +94,6 @@ class GameEngine {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gameState.on('penalty', (data) => {
|
|
||||||
const penalizedPlayer = this.players.find(p => p.name === data.player);
|
|
||||||
if (penalizedPlayer) {
|
|
||||||
penalizedPlayer.state.penaltyTime = data.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
this.gameState.on('period-end', () => {
|
this.gameState.on('period-end', () => {
|
||||||
});
|
});
|
||||||
@ -147,11 +144,7 @@ class GameEngine {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.addEventListener('wheel', (e) => {
|
// Zoom controls disabled for fixed camera view
|
||||||
e.preventDefault();
|
|
||||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
||||||
this.renderer.setZoom(this.renderer.camera.zoom * zoomFactor);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@ -191,7 +184,6 @@ class GameEngine {
|
|||||||
if (this.puckActive) {
|
if (this.puckActive) {
|
||||||
this.puck.update(deltaTime, this.gameState, this.players);
|
this.puck.update(deltaTime, this.gameState, this.players);
|
||||||
this.updateCollisions();
|
this.updateCollisions();
|
||||||
this.renderer.updateCamera(this.puck.position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateEffects(deltaTime);
|
this.updateEffects(deltaTime);
|
||||||
@ -229,16 +221,6 @@ class GameEngine {
|
|||||||
duration: 500,
|
duration: 500,
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Math.random() < 0.1) {
|
|
||||||
const penalizedPlayer = speed1 > speed2 ? player1 : player2;
|
|
||||||
this.gameState.addPenalty(
|
|
||||||
penalizedPlayer.team,
|
|
||||||
penalizedPlayer.name,
|
|
||||||
'Checking',
|
|
||||||
120
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +235,9 @@ class GameEngine {
|
|||||||
|
|
||||||
this.renderEffects();
|
this.renderEffects();
|
||||||
this.renderer.drawUI(this.gameState);
|
this.renderer.drawUI(this.gameState);
|
||||||
this.renderer.drawDebugInfo(this.gameState, this.players, this.puck);
|
|
||||||
|
const selectedPlayer = this.debugSystem ? this.debugSystem.selectedPlayer : null;
|
||||||
|
this.renderer.drawDebugInfo(this.gameState, this.players, this.puck, selectedPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEffects() {
|
renderEffects() {
|
||||||
@ -304,7 +288,6 @@ class GameEngine {
|
|||||||
player.velocity = new Vector2(0, 0);
|
player.velocity = new Vector2(0, 0);
|
||||||
player.state.hasPuck = false;
|
player.state.hasPuck = false;
|
||||||
player.state.energy = 100;
|
player.state.energy = 100;
|
||||||
player.state.penaltyTime = 0;
|
|
||||||
player.aiState.lastAction = 0;
|
player.aiState.lastAction = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -18,14 +18,12 @@ class GameState {
|
|||||||
shots: 0,
|
shots: 0,
|
||||||
saves: 0,
|
saves: 0,
|
||||||
hits: 0,
|
hits: 0,
|
||||||
penalties: [],
|
|
||||||
faceoffWins: 0
|
faceoffWins: 0
|
||||||
},
|
},
|
||||||
away: {
|
away: {
|
||||||
shots: 0,
|
shots: 0,
|
||||||
saves: 0,
|
saves: 0,
|
||||||
hits: 0,
|
hits: 0,
|
||||||
penalties: [],
|
|
||||||
faceoffWins: 0
|
faceoffWins: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -52,10 +50,6 @@ class GameState {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
this.powerPlay = {
|
|
||||||
home: null,
|
|
||||||
away: null
|
|
||||||
};
|
|
||||||
|
|
||||||
this.faceoff = {
|
this.faceoff = {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@ -91,7 +85,6 @@ class GameState {
|
|||||||
this.endPeriod();
|
this.endPeriod();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updatePenalties(deltaTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endPeriod() {
|
endPeriod() {
|
||||||
@ -128,41 +121,7 @@ class GameState {
|
|||||||
this.emit('save', { team, saves: this.stats[team].saves });
|
this.emit('save', { team, saves: this.stats[team].saves });
|
||||||
}
|
}
|
||||||
|
|
||||||
addPenalty(team, player, type, duration = 120) {
|
|
||||||
const penalty = {
|
|
||||||
player,
|
|
||||||
type,
|
|
||||||
duration,
|
|
||||||
timeRemaining: duration
|
|
||||||
};
|
|
||||||
|
|
||||||
this.stats[team].penalties.push(penalty);
|
|
||||||
this.addEvent(`PENALTY - ${team.toUpperCase()} ${player}: ${type}`);
|
|
||||||
|
|
||||||
const oppositeTeam = team === 'home' ? 'away' : 'home';
|
|
||||||
this.powerPlay[oppositeTeam] = Date.now() + (duration * 1000);
|
|
||||||
|
|
||||||
this.emit('penalty', { team, player, type, duration });
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePenalties(deltaTime) {
|
|
||||||
['home', 'away'].forEach(team => {
|
|
||||||
this.stats[team].penalties = this.stats[team].penalties.filter(penalty => {
|
|
||||||
penalty.timeRemaining -= deltaTime * this.gameSpeed;
|
|
||||||
if (penalty.timeRemaining <= 0) {
|
|
||||||
this.emit('penalty-expired', { team, penalty });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
['home', 'away'].forEach(team => {
|
|
||||||
if (this.powerPlay[team] && Date.now() > this.powerPlay[team]) {
|
|
||||||
this.powerPlay[team] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addEvent(description) {
|
addEvent(description) {
|
||||||
const event = {
|
const event = {
|
||||||
@ -271,7 +230,6 @@ class GameState {
|
|||||||
isPaused: this.isPaused,
|
isPaused: this.isPaused,
|
||||||
gameSpeed: this.gameSpeed,
|
gameSpeed: this.gameSpeed,
|
||||||
gameOver: this.gameOver,
|
gameOver: this.gameOver,
|
||||||
powerPlay: { ...this.powerPlay },
|
|
||||||
faceoff: { ...this.faceoff }
|
faceoff: { ...this.faceoff }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,13 @@
|
|||||||
class Player {
|
class Player {
|
||||||
|
/**
|
||||||
|
* Creates a hockey player with physics properties, AI behavior, and game attributes
|
||||||
|
* @param {string} id - Unique identifier for the player
|
||||||
|
* @param {string} name - Player's display name
|
||||||
|
* @param {string} team - Team affiliation ('home' or 'away')
|
||||||
|
* @param {string} position - Hockey position ('LW', 'C', 'RW', 'LD', 'RD', 'G')
|
||||||
|
* @param {number} x - Initial x coordinate
|
||||||
|
* @param {number} y - Initial y coordinate
|
||||||
|
*/
|
||||||
constructor(id, name, team, position, x, y) {
|
constructor(id, name, team, position, x, y) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -10,7 +19,7 @@ class Player {
|
|||||||
this.role = position; // 'LW', 'C', 'RW', 'LD', 'RD', 'G'
|
this.role = position; // 'LW', 'C', 'RW', 'LD', 'RD', 'G'
|
||||||
this.radius = position === 'G' ? 20 : 12;
|
this.radius = position === 'G' ? 20 : 12;
|
||||||
this.mass = position === 'G' ? 2 : 1;
|
this.mass = position === 'G' ? 2 : 1;
|
||||||
this.maxSpeed = position === 'G' ? 150 : 200;
|
this.maxSpeed = position === 'G' ? 200 : 280;
|
||||||
this.acceleration = 800;
|
this.acceleration = 800;
|
||||||
this.restitution = 0.8;
|
this.restitution = 0.8;
|
||||||
|
|
||||||
@ -28,7 +37,6 @@ class Player {
|
|||||||
hasPuck: false,
|
hasPuck: false,
|
||||||
energy: 100,
|
energy: 100,
|
||||||
checking: false,
|
checking: false,
|
||||||
penaltyTime: 0,
|
|
||||||
injured: false
|
injured: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,12 +52,14 @@ class Player {
|
|||||||
this.targetAngle = 0;
|
this.targetAngle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main update loop for the player - handles energy, movement, rotation, and AI behavior
|
||||||
|
* @param {number} deltaTime - Time elapsed since last frame in seconds
|
||||||
|
* @param {Object} gameState - Current game state including rink dimensions and game status
|
||||||
|
* @param {Object} puck - Puck object with position and velocity
|
||||||
|
* @param {Array} players - Array of all players on the ice
|
||||||
|
*/
|
||||||
update(deltaTime, gameState, puck, players) {
|
update(deltaTime, gameState, puck, players) {
|
||||||
if (this.state.penaltyTime > 0) {
|
|
||||||
this.state.penaltyTime -= deltaTime;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateEnergy(deltaTime);
|
this.updateEnergy(deltaTime);
|
||||||
this.updateMovement(deltaTime);
|
this.updateMovement(deltaTime);
|
||||||
this.updateAngle(deltaTime);
|
this.updateAngle(deltaTime);
|
||||||
@ -61,6 +71,11 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates player energy/stamina based on movement and provides recovery when stationary
|
||||||
|
* Energy affects max speed - tired players move slower
|
||||||
|
* @param {number} deltaTime - Time elapsed since last frame in seconds
|
||||||
|
*/
|
||||||
updateEnergy(deltaTime) {
|
updateEnergy(deltaTime) {
|
||||||
const energyDrain = this.velocity.magnitude() / this.maxSpeed * 10 * deltaTime;
|
const energyDrain = this.velocity.magnitude() / this.maxSpeed * 10 * deltaTime;
|
||||||
this.state.energy = Math.max(0, this.state.energy - energyDrain);
|
this.state.energy = Math.max(0, this.state.energy - energyDrain);
|
||||||
@ -74,6 +89,11 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates player physics including movement toward target, velocity limits, and collision bounds
|
||||||
|
* Applies acceleration toward target position with deceleration when close
|
||||||
|
* @param {number} deltaTime - Time elapsed since last frame in seconds
|
||||||
|
*/
|
||||||
updateMovement(deltaTime) {
|
updateMovement(deltaTime) {
|
||||||
const direction = this.targetPosition.subtract(this.position).normalize();
|
const direction = this.targetPosition.subtract(this.position).normalize();
|
||||||
const distance = this.position.distance(this.targetPosition);
|
const distance = this.position.distance(this.targetPosition);
|
||||||
@ -97,6 +117,10 @@ class Player {
|
|||||||
this.keepInBounds();
|
this.keepInBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates player rotation to face target angle with smooth turning animation
|
||||||
|
* @param {number} deltaTime - Time elapsed since last frame in seconds
|
||||||
|
*/
|
||||||
updateAngle(deltaTime) {
|
updateAngle(deltaTime) {
|
||||||
let angleDiff = this.targetAngle - this.angle;
|
let angleDiff = this.targetAngle - this.angle;
|
||||||
angleDiff = Physics.wrapAngle(angleDiff);
|
angleDiff = Physics.wrapAngle(angleDiff);
|
||||||
@ -108,6 +132,13 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main AI decision making system - handles reaction timing, faceoffs, and behavioral switching
|
||||||
|
* Delegates to specific behavior methods based on puck possession
|
||||||
|
* @param {Object} gameState - Current game state including faceoff status
|
||||||
|
* @param {Object} puck - Puck object with position and velocity
|
||||||
|
* @param {Array} players - Array of all players on the ice
|
||||||
|
*/
|
||||||
updateAI(gameState, puck, players) {
|
updateAI(gameState, puck, players) {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
if (currentTime - this.aiState.lastAction < this.aiState.reactionTime) {
|
if (currentTime - this.aiState.lastAction < this.aiState.reactionTime) {
|
||||||
@ -134,6 +165,14 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offensive AI behavior when player has possession of the puck
|
||||||
|
* Prioritizes shooting, then passing under pressure, then advancing toward goal
|
||||||
|
* @param {Object} gameState - Current game state with rink dimensions
|
||||||
|
* @param {Object} puck - Puck object to shoot or pass
|
||||||
|
* @param {Array} teammates - Array of teammate player objects
|
||||||
|
* @param {Array} opponents - Array of opposing player objects
|
||||||
|
*/
|
||||||
behaviorWithPuck(gameState, puck, teammates, opponents) {
|
behaviorWithPuck(gameState, puck, teammates, opponents) {
|
||||||
const enemyGoal = this.team === 'home' ?
|
const enemyGoal = this.team === 'home' ?
|
||||||
new Vector2(gameState.rink.width - 50, gameState.rink.centerY) :
|
new Vector2(gameState.rink.width - 50, gameState.rink.centerY) :
|
||||||
@ -164,6 +203,15 @@ class Player {
|
|||||||
this.advanceTowardGoal(enemyGoal, opponents, gameState.rink);
|
this.advanceTowardGoal(enemyGoal, opponents, gameState.rink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defensive AI behavior when player doesn't have puck possession
|
||||||
|
* Chooses between chasing loose puck, defending against opponents, or maintaining formation
|
||||||
|
* @param {Object} gameState - Current game state
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Array} teammates - Array of teammate player objects
|
||||||
|
* @param {Array} opponents - Array of opposing player objects
|
||||||
|
* @param {number} distanceToPuck - Pre-calculated distance to puck
|
||||||
|
*/
|
||||||
behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck) {
|
behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck) {
|
||||||
const puckOwner = opponents.find(p => p.state.hasPuck) || teammates.find(p => p.state.hasPuck);
|
const puckOwner = opponents.find(p => p.state.hasPuck) || teammates.find(p => p.state.hasPuck);
|
||||||
const isClosestToPuck = this.isClosestPlayerToPuck(puck, teammates);
|
const isClosestToPuck = this.isClosestPlayerToPuck(puck, teammates);
|
||||||
@ -173,7 +221,14 @@ class Player {
|
|||||||
// Only chase if this player is closest to the puck on their team
|
// Only chase if this player is closest to the puck on their team
|
||||||
this.chasePuck(puck);
|
this.chasePuck(puck);
|
||||||
} else if (puckOwner && puckOwner.team !== this.team) {
|
} else if (puckOwner && puckOwner.team !== this.team) {
|
||||||
if (distanceToPuck < 150 && Math.random() < 0.2) {
|
// Check if this player is the closest defender to the puck carrier
|
||||||
|
const isClosestDefender = this.isClosestDefenderToPuckCarrier(puckOwner, teammates);
|
||||||
|
|
||||||
|
if (isClosestDefender) {
|
||||||
|
// Closest defender aggressively targets the puck carrier
|
||||||
|
this.moveToPosition(puckOwner.position);
|
||||||
|
this.aiState.behavior = 'aggressive_pressure';
|
||||||
|
} else if (distanceToPuck < 150 && Math.random() < 0.2) {
|
||||||
this.checkPlayer(puckOwner);
|
this.checkPlayer(puckOwner);
|
||||||
} else {
|
} else {
|
||||||
this.defendPosition(gameState, puckOwner);
|
this.defendPosition(gameState, puckOwner);
|
||||||
@ -183,6 +238,13 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Goalie-specific AI behavior - stays in crease and tracks puck movement
|
||||||
|
* Positions between puck and goal, with more aggressive positioning when puck is close
|
||||||
|
* @param {Object} gameState - Current game state with rink dimensions
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Array} players - Array of all players (unused but maintained for consistency)
|
||||||
|
*/
|
||||||
updateGoalie(gameState, puck, players) {
|
updateGoalie(gameState, puck, players) {
|
||||||
const goal = this.team === 'home' ?
|
const goal = this.team === 'home' ?
|
||||||
new Vector2(10, gameState.rink.centerY) :
|
new Vector2(10, gameState.rink.centerY) :
|
||||||
@ -208,11 +270,22 @@ class Player {
|
|||||||
this.targetPosition.y = Math.max(crease.y, Math.min(crease.y + crease.height, this.targetPosition.y));
|
this.targetPosition.y = Math.max(crease.y, Math.min(crease.y + crease.height, this.targetPosition.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets player target to chase after a loose puck
|
||||||
|
* @param {Object} puck - Puck object with position to chase
|
||||||
|
*/
|
||||||
chasePuck(puck) {
|
chasePuck(puck) {
|
||||||
this.moveToPosition(puck.position);
|
this.moveToPosition(puck.position);
|
||||||
this.aiState.behavior = 'chasing';
|
this.aiState.behavior = 'chasing';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shoots the puck toward a target with power and accuracy based on player attributes
|
||||||
|
* Applies random spread based on shooting accuracy - better shooters are more precise
|
||||||
|
* @param {Object} puck - Puck object to shoot
|
||||||
|
* @param {Vector2} target - Target position to aim for
|
||||||
|
* @returns {boolean} True if shot was taken
|
||||||
|
*/
|
||||||
shoot(puck, target) {
|
shoot(puck, target) {
|
||||||
const direction = target.subtract(puck.position).normalize();
|
const direction = target.subtract(puck.position).normalize();
|
||||||
const power = this.attributes.shooting / 100 * 800;
|
const power = this.attributes.shooting / 100 * 800;
|
||||||
@ -227,10 +300,17 @@ class Player {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the puck to a teammate with power scaled by distance
|
||||||
|
* Closer passes are softer, longer passes are harder
|
||||||
|
* @param {Object} puck - Puck object to pass
|
||||||
|
* @param {Object} target - Target player object to pass to
|
||||||
|
* @returns {boolean} True if pass was made
|
||||||
|
*/
|
||||||
pass(puck, target) {
|
pass(puck, target) {
|
||||||
const direction = target.position.subtract(puck.position).normalize();
|
const direction = target.position.subtract(puck.position).normalize();
|
||||||
const distance = puck.position.distance(target.position);
|
const distance = puck.position.distance(target.position);
|
||||||
const power = Math.min(600, distance * 2);
|
const power = Math.min(800, distance * 2.5);
|
||||||
|
|
||||||
puck.velocity = direction.multiply(power);
|
puck.velocity = direction.multiply(power);
|
||||||
this.state.hasPuck = false;
|
this.state.hasPuck = false;
|
||||||
@ -238,6 +318,12 @@ class Player {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to body check an opponent player
|
||||||
|
* If close enough, applies knockback force; otherwise moves toward target
|
||||||
|
* @param {Object} target - Target player to check
|
||||||
|
* @returns {boolean} True if check was successful (contact made)
|
||||||
|
*/
|
||||||
checkPlayer(target) {
|
checkPlayer(target) {
|
||||||
if (this.position.distance(target.position) < 30) {
|
if (this.position.distance(target.position) < 30) {
|
||||||
target.velocity = target.velocity.add(
|
target.velocity = target.velocity.add(
|
||||||
@ -250,11 +336,21 @@ class Player {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the player's target position and facing angle
|
||||||
|
* @param {Vector2} target - Target position to move toward
|
||||||
|
*/
|
||||||
moveToPosition(target) {
|
moveToPosition(target) {
|
||||||
this.targetPosition = target.copy();
|
this.targetPosition = target.copy();
|
||||||
this.targetAngle = target.subtract(this.position).angle();
|
this.targetAngle = target.subtract(this.position).angle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions player defensively between opponent and own goal
|
||||||
|
* Uses interpolation to stay closer to opponent than goal
|
||||||
|
* @param {Object} gameState - Current game state with rink dimensions
|
||||||
|
* @param {Object} opponent - Opponent player to defend against
|
||||||
|
*/
|
||||||
defendPosition(gameState, opponent) {
|
defendPosition(gameState, opponent) {
|
||||||
const ownGoal = this.team === 'home' ?
|
const ownGoal = this.team === 'home' ?
|
||||||
new Vector2(50, gameState.rink.centerY) :
|
new Vector2(50, gameState.rink.centerY) :
|
||||||
@ -265,11 +361,24 @@ class Player {
|
|||||||
this.aiState.behavior = 'defending';
|
this.aiState.behavior = 'defending';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves player to their calculated formation position based on game context
|
||||||
|
* @param {Object} gameState - Current game state
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Array} players - Array of all players for formation calculation
|
||||||
|
*/
|
||||||
moveToFormationPosition(gameState, puck, players) {
|
moveToFormationPosition(gameState, puck, players) {
|
||||||
this.moveToPosition(this.getFormationPosition(gameState, puck, players));
|
this.moveToPosition(this.getFormationPosition(gameState, puck, players));
|
||||||
this.aiState.behavior = 'formation';
|
this.aiState.behavior = 'formation';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates ideal formation position for this player based on team state and puck location
|
||||||
|
* @param {Object} gameState - Current game state with rink dimensions
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Array} players - Array of all players to determine puck ownership
|
||||||
|
* @returns {Vector2} Calculated formation position
|
||||||
|
*/
|
||||||
getFormationPosition(gameState, puck, players) {
|
getFormationPosition(gameState, puck, players) {
|
||||||
const side = this.team === 'home' ? -1 : 1;
|
const side = this.team === 'home' ? -1 : 1;
|
||||||
const rink = gameState.rink;
|
const rink = gameState.rink;
|
||||||
@ -282,6 +391,14 @@ class Player {
|
|||||||
return this.getContextualPosition(rink, side, isAttacking, puck);
|
return this.getContextualPosition(rink, side, isAttacking, puck);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this player's team is in attacking or defending mode
|
||||||
|
* Based on puck possession and puck location on the rink
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Object} puckOwner - Player object who has puck possession (null if loose)
|
||||||
|
* @param {Object} rink - Rink object with dimensions
|
||||||
|
* @returns {boolean} True if team is attacking, false if defending
|
||||||
|
*/
|
||||||
determineTeamState(puck, puckOwner, rink) {
|
determineTeamState(puck, puckOwner, rink) {
|
||||||
const homeAttackingZone = rink.width * 0.67; // Right side for home team
|
const homeAttackingZone = rink.width * 0.67; // Right side for home team
|
||||||
const awayAttackingZone = rink.width * 0.33; // Left side for away team
|
const awayAttackingZone = rink.width * 0.33; // Left side for away team
|
||||||
@ -304,6 +421,15 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates specific position for player based on role, team state, and puck influence
|
||||||
|
* Different formations for attacking vs defending, with puck tracking adjustments
|
||||||
|
* @param {Object} rink - Rink object with dimensions and center points
|
||||||
|
* @param {number} side - Team side multiplier (-1 for home, 1 for away)
|
||||||
|
* @param {boolean} isAttacking - Whether team is in attacking formation
|
||||||
|
* @param {Object} puck - Puck object for positional influence
|
||||||
|
* @returns {Vector2} Calculated contextual position
|
||||||
|
*/
|
||||||
getContextualPosition(rink, side, isAttacking, puck) {
|
getContextualPosition(rink, side, isAttacking, puck) {
|
||||||
const centerY = rink.centerY;
|
const centerY = rink.centerY;
|
||||||
const puckInfluenceX = (puck.position.x - rink.centerX) * 0.3; // Follow puck horizontally
|
const puckInfluenceX = (puck.position.x - rink.centerX) * 0.3; // Follow puck horizontally
|
||||||
@ -380,6 +506,11 @@ class Player {
|
|||||||
return new Vector2(baseX, baseY);
|
return new Vector2(baseX, baseY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the closest player from a given array of players
|
||||||
|
* @param {Array} players - Array of player objects to search through
|
||||||
|
* @returns {Object|null} Nearest player object, or null if no players provided
|
||||||
|
*/
|
||||||
findNearestPlayer(players) {
|
findNearestPlayer(players) {
|
||||||
let nearest = null;
|
let nearest = null;
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
@ -395,6 +526,12 @@ class Player {
|
|||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates teammates to find the best pass target based on distance, skill, and opponent blocking
|
||||||
|
* @param {Array} teammates - Array of teammate player objects
|
||||||
|
* @param {Array} opponents - Array of opponent players that might block the pass
|
||||||
|
* @returns {Object|null} Best teammate to pass to, or null if no good options
|
||||||
|
*/
|
||||||
findBestPassTarget(teammates, opponents) {
|
findBestPassTarget(teammates, opponents) {
|
||||||
let bestTarget = null;
|
let bestTarget = null;
|
||||||
let bestScore = -1;
|
let bestScore = -1;
|
||||||
@ -426,6 +563,13 @@ class Player {
|
|||||||
return bestTarget;
|
return bestTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this player is the closest non-goalie teammate to the puck
|
||||||
|
* Used to decide who should chase loose pucks
|
||||||
|
* @param {Object} puck - Puck object with position
|
||||||
|
* @param {Array} teammates - Array of teammate player objects
|
||||||
|
* @returns {boolean} True if this player is closest to puck on their team
|
||||||
|
*/
|
||||||
isClosestPlayerToPuck(puck, teammates) {
|
isClosestPlayerToPuck(puck, teammates) {
|
||||||
// Check if this player (excluding goalies) is closest to the puck on their team
|
// Check if this player (excluding goalies) is closest to the puck on their team
|
||||||
if (this.role === 'G' || this.state.hasPuck) return false;
|
if (this.role === 'G' || this.state.hasPuck) return false;
|
||||||
@ -450,6 +594,43 @@ class Player {
|
|||||||
return closestPlayer === this;
|
return closestPlayer === this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this player is the closest defender to the puck carrier
|
||||||
|
* @param {Object} puckCarrier - The player who has the puck
|
||||||
|
* @param {Array} teammates - Array of teammate player objects
|
||||||
|
* @returns {boolean} True if this player is closest to puck carrier on their team
|
||||||
|
*/
|
||||||
|
isClosestDefenderToPuckCarrier(puckCarrier, teammates) {
|
||||||
|
// Skip goalies
|
||||||
|
if (this.role === 'G') return false;
|
||||||
|
|
||||||
|
const myDistance = this.position.distance(puckCarrier.position);
|
||||||
|
|
||||||
|
// Include self in the list to compare against (excluding goalies)
|
||||||
|
const allTeamPlayers = [this, ...teammates.filter(t => t.role !== 'G')];
|
||||||
|
|
||||||
|
// Find the closest player to the puck carrier
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
let closestPlayer = null;
|
||||||
|
|
||||||
|
allTeamPlayers.forEach(player => {
|
||||||
|
const distance = player.position.distance(puckCarrier.position);
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closestDistance = distance;
|
||||||
|
closestPlayer = player;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return closestPlayer === this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether player has a clear shooting angle to goal
|
||||||
|
* Checks if opponents are blocking the direct path to goal
|
||||||
|
* @param {Vector2} goalPosition - Target goal position
|
||||||
|
* @param {Array} opponents - Array of opponent players that might block shot
|
||||||
|
* @returns {boolean} True if shooting angle is clear
|
||||||
|
*/
|
||||||
hasGoodShootingAngle(goalPosition, opponents) {
|
hasGoodShootingAngle(goalPosition, opponents) {
|
||||||
// Check if there's a clear line to goal (simplified check)
|
// Check if there's a clear line to goal (simplified check)
|
||||||
const directionToGoal = goalPosition.subtract(this.position).normalize();
|
const directionToGoal = goalPosition.subtract(this.position).normalize();
|
||||||
@ -474,6 +655,13 @@ class Player {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intelligently advances player with puck toward the opponent's goal
|
||||||
|
* Uses pathfinding to avoid opponents and direct approach when close
|
||||||
|
* @param {Vector2} goalPosition - Target goal position to advance toward
|
||||||
|
* @param {Array} opponents - Array of opponent players to avoid
|
||||||
|
* @param {Object} rink - Rink object with boundary dimensions
|
||||||
|
*/
|
||||||
advanceTowardGoal(goalPosition, opponents, rink) {
|
advanceTowardGoal(goalPosition, opponents, rink) {
|
||||||
// Create an intelligent path toward the goal
|
// Create an intelligent path toward the goal
|
||||||
let targetPosition = goalPosition.copy();
|
let targetPosition = goalPosition.copy();
|
||||||
@ -499,6 +687,14 @@ class Player {
|
|||||||
this.moveToPosition(targetPosition);
|
this.moveToPosition(targetPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates path adjustments to avoid opponents while advancing toward goal
|
||||||
|
* Creates lateral movement to navigate around blocking opponents
|
||||||
|
* @param {Vector2} goalPosition - Target goal position
|
||||||
|
* @param {Array} opponents - Array of opponent players to avoid
|
||||||
|
* @param {Object} rink - Rink object for boundary awareness
|
||||||
|
* @returns {Vector2} Position adjustment vector to avoid opponents
|
||||||
|
*/
|
||||||
findBestPathToGoal(goalPosition, opponents, rink) {
|
findBestPathToGoal(goalPosition, opponents, rink) {
|
||||||
const currentPos = this.position;
|
const currentPos = this.position;
|
||||||
const adjustment = new Vector2(0, 0);
|
const adjustment = new Vector2(0, 0);
|
||||||
@ -537,19 +733,27 @@ class Player {
|
|||||||
return adjustment;
|
return adjustment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrains player position to stay within rink boundaries
|
||||||
|
* Uses hardcoded rink dimensions of 1000x600
|
||||||
|
*/
|
||||||
keepInBounds() {
|
keepInBounds() {
|
||||||
this.position.x = Math.max(this.radius, Math.min(1000 - this.radius, this.position.x));
|
this.position.x = Math.max(this.radius, Math.min(1000 - this.radius, this.position.x));
|
||||||
this.position.y = Math.max(this.radius, Math.min(600 - this.radius, this.position.y));
|
this.position.y = Math.max(this.radius, Math.min(600 - this.radius, this.position.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the player on the canvas with team colors, puck indicator, and role text
|
||||||
|
* @param {CanvasRenderingContext2D} ctx - 2D rendering context for drawing
|
||||||
|
*/
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(this.position.x, this.position.y);
|
ctx.translate(this.position.x, this.position.y);
|
||||||
ctx.rotate(this.angle);
|
ctx.rotate(this.angle);
|
||||||
|
|
||||||
ctx.fillStyle = this.team === 'home' ? '#4a90e2' : '#e24a4a';
|
ctx.fillStyle = this.team === 'home' ? '#4a90e2' : '#e24a4a';
|
||||||
ctx.strokeStyle = '#fff';
|
ctx.strokeStyle = this.state.hasPuck ? '#000' : '#fff';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = this.state.hasPuck ? 3 : 2;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
||||||
@ -571,6 +775,12 @@ class Player {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles player positioning during faceoff situations
|
||||||
|
* Centers participate directly, other positions maintain legal distance from faceoff circle
|
||||||
|
* @param {Object} gameState - Current game state with faceoff information
|
||||||
|
* @param {Array} players - Array of all players for positioning context
|
||||||
|
*/
|
||||||
handleFaceoffPositioning(gameState, players) {
|
handleFaceoffPositioning(gameState, players) {
|
||||||
const faceoffPos = this.getFaceoffPosition(gameState, players);
|
const faceoffPos = this.getFaceoffPosition(gameState, players);
|
||||||
this.moveToPosition(faceoffPos);
|
this.moveToPosition(faceoffPos);
|
||||||
@ -585,6 +795,13 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates legal faceoff positioning for each player role
|
||||||
|
* Centers face off directly, other positions must stay outside faceoff circle per hockey rules
|
||||||
|
* @param {Object} gameState - Current game state with faceoff location and rink info
|
||||||
|
* @param {Array} players - Array of all players (unused but maintained for consistency)
|
||||||
|
* @returns {Vector2} Legal faceoff position for this player's role
|
||||||
|
*/
|
||||||
getFaceoffPosition(gameState, players) {
|
getFaceoffPosition(gameState, players) {
|
||||||
const faceoffLocation = gameState.faceoff.location;
|
const faceoffLocation = gameState.faceoff.location;
|
||||||
const side = this.team === 'home' ? -1 : 1;
|
const side = this.team === 'home' ? -1 : 1;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class Puck {
|
|||||||
this.updatePosition(deltaTime);
|
this.updatePosition(deltaTime);
|
||||||
this.checkBoardCollisions(gameState);
|
this.checkBoardCollisions(gameState);
|
||||||
this.checkPlayerCollisions(players, gameState);
|
this.checkPlayerCollisions(players, gameState);
|
||||||
|
this.checkPuckPossession(players);
|
||||||
this.updateTrail();
|
this.updateTrail();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,6 +27,48 @@ class Puck {
|
|||||||
this.position = this.position.add(this.velocity.multiply(deltaTime));
|
this.position = this.position.add(this.velocity.multiply(deltaTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkPuckPossession(players) {
|
||||||
|
const puckCarrier = players.find(player => player.state.hasPuck);
|
||||||
|
if (!puckCarrier) return;
|
||||||
|
|
||||||
|
// Check if any opponent is trying to steal the puck
|
||||||
|
const opponents = players.filter(p => p.team !== puckCarrier.team && p.role !== 'G');
|
||||||
|
const nearbyOpponent = opponents.find(opponent => {
|
||||||
|
const distanceToCarrier = opponent.position.distance(puckCarrier.position);
|
||||||
|
const distanceToPuck = opponent.position.distance(this.position);
|
||||||
|
return distanceToCarrier < 25 && distanceToPuck < 20;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nearbyOpponent) {
|
||||||
|
// Opponent is close enough to potentially steal the puck
|
||||||
|
const stealChance = 0.005; // 0.5% chance per frame (roughly 30% per second at 60fps)
|
||||||
|
if (Math.random() < stealChance) {
|
||||||
|
puckCarrier.state.hasPuck = false;
|
||||||
|
nearbyOpponent.state.hasPuck = true;
|
||||||
|
this.lastPlayerTouch = nearbyOpponent;
|
||||||
|
this.lastTeamTouch = nearbyOpponent.team;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky puck logic - keep puck attached to carrier
|
||||||
|
const stickDistance = 15; // Distance puck stays from player
|
||||||
|
const directionToPlayer = puckCarrier.position.subtract(this.position).normalize();
|
||||||
|
|
||||||
|
// Position puck slightly in front of player based on their movement direction
|
||||||
|
let puckOffset;
|
||||||
|
if (puckCarrier.velocity.magnitude() > 10) {
|
||||||
|
// When moving, position puck in front of player
|
||||||
|
puckOffset = puckCarrier.velocity.normalize().multiply(stickDistance);
|
||||||
|
} else {
|
||||||
|
// When stationary, keep puck close
|
||||||
|
puckOffset = directionToPlayer.multiply(-stickDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.position = puckCarrier.position.add(puckOffset);
|
||||||
|
this.velocity = puckCarrier.velocity.multiply(1.0); // Match player velocity
|
||||||
|
}
|
||||||
|
|
||||||
updateTrail() {
|
updateTrail() {
|
||||||
if (this.velocity.magnitude() > 50) {
|
if (this.velocity.magnitude() > 50) {
|
||||||
this.trail.unshift({
|
this.trail.unshift({
|
||||||
@ -99,8 +142,6 @@ class Puck {
|
|||||||
let closestDistance = Infinity;
|
let closestDistance = Infinity;
|
||||||
|
|
||||||
players.forEach(player => {
|
players.forEach(player => {
|
||||||
if (player.state.penaltyTime > 0) return;
|
|
||||||
|
|
||||||
const distance = this.position.distance(player.position);
|
const distance = this.position.distance(player.position);
|
||||||
const collisionDistance = this.radius + player.radius;
|
const collisionDistance = this.radius + player.radius;
|
||||||
|
|
||||||
@ -113,11 +154,11 @@ class Puck {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (closestPlayer) {
|
if (closestPlayer) {
|
||||||
this.handlePlayerCollision(closestPlayer, gameState);
|
this.handlePlayerCollision(closestPlayer, gameState, players);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayerCollision(player, gameState) {
|
handlePlayerCollision(player, gameState, players) {
|
||||||
const distance = this.position.distance(player.position);
|
const distance = this.position.distance(player.position);
|
||||||
const minDistance = this.radius + player.radius;
|
const minDistance = this.radius + player.radius;
|
||||||
|
|
||||||
@ -137,15 +178,15 @@ class Puck {
|
|||||||
|
|
||||||
this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution));
|
this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution));
|
||||||
|
|
||||||
if (this.velocity.magnitude() < 100 && player.role !== 'G') {
|
if (player.role !== 'G') {
|
||||||
this.pickupPuck(player, gameState);
|
this.pickupPuck(player, gameState, players);
|
||||||
} else if (player.role === 'G' && this.velocity.magnitude() > 50) {
|
} else if (player.role === 'G' && this.velocity.magnitude() > 50) {
|
||||||
this.handleGoalieSave(player, gameState);
|
this.handleGoalieSave(player, gameState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pickupPuck(player, gameState) {
|
pickupPuck(player, gameState, players) {
|
||||||
if (player.state.hasPuck) return;
|
if (player.state.hasPuck) return;
|
||||||
|
|
||||||
players.forEach(p => p.state.hasPuck = false);
|
players.forEach(p => p.state.hasPuck = false);
|
||||||
@ -185,7 +226,7 @@ class Puck {
|
|||||||
this.trail = [];
|
this.trail = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
pass(target, power = 300) {
|
pass(target, power = 500) {
|
||||||
const direction = target.subtract(this.position).normalize();
|
const direction = target.subtract(this.position).normalize();
|
||||||
this.velocity = direction.multiply(power);
|
this.velocity = direction.multiply(power);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,7 +84,6 @@ class AISystem {
|
|||||||
gameTime: gameState.timeRemaining,
|
gameTime: gameState.timeRemaining,
|
||||||
period: gameState.period,
|
period: gameState.period,
|
||||||
scoreGap: gameState.homeScore - gameState.awayScore,
|
scoreGap: gameState.homeScore - gameState.awayScore,
|
||||||
powerPlay: gameState.powerPlay,
|
|
||||||
zone: this.determinePuckZone(puck, gameState)
|
zone: this.determinePuckZone(puck, gameState)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,9 +103,6 @@ class AISystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectFormation(context) {
|
selectFormation(context) {
|
||||||
if (context.powerPlay.home || context.powerPlay.away) {
|
|
||||||
return context.powerPlay.home ? 'offensive' : 'defensive';
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (context.zone) {
|
switch (context.zone) {
|
||||||
case 'offensive':
|
case 'offensive':
|
||||||
@ -124,7 +120,6 @@ class AISystem {
|
|||||||
if (context.gameTime < 300) urgency += 0.3;
|
if (context.gameTime < 300) urgency += 0.3;
|
||||||
if (context.gameTime < 120) urgency += 0.3;
|
if (context.gameTime < 120) urgency += 0.3;
|
||||||
if (Math.abs(context.scoreGap) > 1) urgency += 0.2;
|
if (Math.abs(context.scoreGap) > 1) urgency += 0.2;
|
||||||
if (context.powerPlay.home || context.powerPlay.away) urgency += 0.4;
|
|
||||||
|
|
||||||
return Math.min(1, urgency);
|
return Math.min(1, urgency);
|
||||||
}
|
}
|
||||||
@ -181,6 +176,9 @@ class AISystem {
|
|||||||
const distanceToPuck = player.position.distance(context.puckPosition);
|
const distanceToPuck = player.position.distance(context.puckPosition);
|
||||||
const isNearPuck = distanceToPuck < 100;
|
const isNearPuck = distanceToPuck < 100;
|
||||||
const isClosestToPuck = context.closestPlayers[player.team] === player;
|
const isClosestToPuck = context.closestPlayers[player.team] === player;
|
||||||
|
const isClosestDefender = context.puckOwner &&
|
||||||
|
context.puckOwner.team !== player.team &&
|
||||||
|
context.closestPlayers[player.team] === player;
|
||||||
|
|
||||||
if (player.state.hasPuck) {
|
if (player.state.hasPuck) {
|
||||||
player.aiState.behavior = 'puck_carrier';
|
player.aiState.behavior = 'puck_carrier';
|
||||||
@ -188,6 +186,10 @@ class AISystem {
|
|||||||
} else if (context.puckOwner && context.puckOwner.team === player.team) {
|
} else if (context.puckOwner && context.puckOwner.team === player.team) {
|
||||||
player.aiState.behavior = 'support';
|
player.aiState.behavior = 'support';
|
||||||
this.executeSupportBehavior(player, context);
|
this.executeSupportBehavior(player, context);
|
||||||
|
} else if (isClosestDefender) {
|
||||||
|
// Closest defender directly targets the puck carrier
|
||||||
|
player.aiState.behavior = 'aggressive_pressure';
|
||||||
|
this.executeAggressivePressureBehavior(player, context);
|
||||||
} else if (context.puckOwner && context.puckOwner.team !== player.team) {
|
} else if (context.puckOwner && context.puckOwner.team !== player.team) {
|
||||||
player.aiState.behavior = 'pressure';
|
player.aiState.behavior = 'pressure';
|
||||||
this.executePressureBehavior(player, context);
|
this.executePressureBehavior(player, context);
|
||||||
@ -226,6 +228,14 @@ class AISystem {
|
|||||||
player.aiState.action = 'support';
|
player.aiState.action = 'support';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeAggressivePressureBehavior(player, context) {
|
||||||
|
const opponent = context.puckOwner;
|
||||||
|
|
||||||
|
// Closest defender directly targets the puck carrier's position
|
||||||
|
player.targetPosition = opponent.position.copy();
|
||||||
|
player.aiState.action = 'aggressive_pressure';
|
||||||
|
}
|
||||||
|
|
||||||
executePressureBehavior(player, context) {
|
executePressureBehavior(player, context) {
|
||||||
const opponent = context.puckOwner;
|
const opponent = context.puckOwner;
|
||||||
const pressurePosition = this.calculatePressurePosition(player, opponent, context);
|
const pressurePosition = this.calculatePressurePosition(player, opponent, context);
|
||||||
|
|||||||
464
src/systems/debug-system.js
Normal file
464
src/systems/debug-system.js
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
class DebugSystem {
|
||||||
|
constructor(gameEngine) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.isVisible = false;
|
||||||
|
this.selectedPlayer = null;
|
||||||
|
this.updateInterval = null;
|
||||||
|
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.setupGlobalDebugFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Debug toggle button
|
||||||
|
document.getElementById('debug-toggle').addEventListener('click', () => {
|
||||||
|
this.toggleDebugPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close debug panel
|
||||||
|
document.getElementById('debug-close').addEventListener('click', () => {
|
||||||
|
this.hideDebugPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Canvas click for player selection
|
||||||
|
this.gameEngine.canvas.addEventListener('click', (e) => {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.handleCanvasClick(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor debug mode changes (triggered by existing D key handler)
|
||||||
|
let lastDebugMode = window.debugMode;
|
||||||
|
setInterval(() => {
|
||||||
|
if (window.debugMode !== lastDebugMode) {
|
||||||
|
if (window.debugMode && !this.isVisible) {
|
||||||
|
this.showDebugPanel();
|
||||||
|
} else if (!window.debugMode && this.isVisible) {
|
||||||
|
this.hideDebugPanel();
|
||||||
|
}
|
||||||
|
lastDebugMode = window.debugMode;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDebugPanel() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hideDebugPanel();
|
||||||
|
} else {
|
||||||
|
this.showDebugPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDebugPanel() {
|
||||||
|
this.isVisible = true;
|
||||||
|
document.getElementById('debug-panel').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Start real-time updates
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.updateDebugInfo();
|
||||||
|
}, 100); // Update 10 times per second
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
this.updateDebugInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDebugPanel() {
|
||||||
|
this.isVisible = false;
|
||||||
|
document.getElementById('debug-panel').classList.add('hidden');
|
||||||
|
|
||||||
|
// Stop real-time updates
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCanvasClick(e) {
|
||||||
|
const rect = this.gameEngine.canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Find closest player to click position
|
||||||
|
let closestPlayer = null;
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
|
||||||
|
this.gameEngine.players.forEach(player => {
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(player.position.x - x, 2) +
|
||||||
|
Math.pow(player.position.y - y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < player.radius + 10 && distance < closestDistance) {
|
||||||
|
closestPlayer = player;
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (closestPlayer) {
|
||||||
|
this.selectPlayer(closestPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPlayer(player) {
|
||||||
|
// Remove previous selection styling
|
||||||
|
document.querySelectorAll('.debug-player').forEach(el => {
|
||||||
|
el.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectedPlayer = player;
|
||||||
|
|
||||||
|
// Add selection styling to the clicked player
|
||||||
|
const playerElement = document.querySelector(`[data-player-id="${player.id}"]`);
|
||||||
|
if (playerElement) {
|
||||||
|
playerElement.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSelectedPlayerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDebugInfo() {
|
||||||
|
if (!this.isVisible) return;
|
||||||
|
|
||||||
|
this.updateGameStateInfo();
|
||||||
|
this.updatePuckInfo();
|
||||||
|
this.updatePlayersInfo();
|
||||||
|
if (this.selectedPlayer) {
|
||||||
|
this.updateSelectedPlayerInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGameStateInfo() {
|
||||||
|
const gameState = this.gameEngine.gameState;
|
||||||
|
const info = `
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Period:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.getPeriodName()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Time:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.formatTime(gameState.timeRemaining)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Score:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.homeScore} - ${gameState.awayScore}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Paused:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.isPaused}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Speed:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.gameSpeed}x</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Faceoff Active:</span>
|
||||||
|
<span class="debug-attribute-value">${gameState.faceoff.isActive}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('debug-game-state').innerHTML = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePuckInfo() {
|
||||||
|
const puck = this.gameEngine.puck;
|
||||||
|
const info = `
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Position:</span>
|
||||||
|
<span class="debug-attribute-value debug-coords">(${puck.position.x.toFixed(1)}, ${puck.position.y.toFixed(1)})</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Velocity:</span>
|
||||||
|
<span class="debug-attribute-value debug-coords">(${puck.velocity.x.toFixed(1)}, ${puck.velocity.y.toFixed(1)})</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Speed:</span>
|
||||||
|
<span class="debug-attribute-value">${puck.getSpeed().toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Active:</span>
|
||||||
|
<span class="debug-attribute-value">${this.gameEngine.puckActive}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Owner:</span>
|
||||||
|
<span class="debug-attribute-value">${puck.lastTouchedBy || 'None'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('debug-puck').innerHTML = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayersInfo() {
|
||||||
|
const homePlayers = this.gameEngine.players.filter(p => p.team === 'home');
|
||||||
|
const awayPlayers = this.gameEngine.players.filter(p => p.team === 'away');
|
||||||
|
|
||||||
|
this.renderPlayersList('debug-home-players', homePlayers);
|
||||||
|
this.renderPlayersList('debug-away-players', awayPlayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPlayersList(containerId, players) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
|
||||||
|
// Only create elements if they don't exist yet (prevent flickering)
|
||||||
|
if (container.children.length === 0) {
|
||||||
|
this.createPlayerElements(container, players);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing elements instead of recreating them
|
||||||
|
players.forEach((player, index) => {
|
||||||
|
const playerDiv = container.children[index];
|
||||||
|
if (!playerDiv) return;
|
||||||
|
|
||||||
|
// Update selection state
|
||||||
|
if (this.selectedPlayer && this.selectedPlayer.id === player.id) {
|
||||||
|
playerDiv.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
playerDiv.classList.remove('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dynamic content only
|
||||||
|
const coordsElement = playerDiv.querySelector('.debug-coords');
|
||||||
|
const speedElement = playerDiv.querySelector('.debug-speed');
|
||||||
|
const energyElement = playerDiv.querySelector('.debug-energy');
|
||||||
|
const puckElement = playerDiv.querySelector('.debug-puck');
|
||||||
|
const behaviorElement = playerDiv.querySelector('.debug-behavior');
|
||||||
|
|
||||||
|
if (coordsElement) coordsElement.textContent = `Pos: (${player.position.x.toFixed(0)}, ${player.position.y.toFixed(0)})`;
|
||||||
|
if (speedElement) speedElement.textContent = player.velocity.magnitude().toFixed(1);
|
||||||
|
if (energyElement) energyElement.textContent = `${player.state.energy.toFixed(0)}%`;
|
||||||
|
if (puckElement) puckElement.textContent = player.state.hasPuck;
|
||||||
|
if (behaviorElement) behaviorElement.textContent = player.aiState.behavior;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlayerElements(container, players) {
|
||||||
|
players.forEach(player => {
|
||||||
|
const playerDiv = document.createElement('div');
|
||||||
|
playerDiv.className = 'debug-player';
|
||||||
|
playerDiv.setAttribute('data-player-id', player.id);
|
||||||
|
|
||||||
|
playerDiv.innerHTML = `
|
||||||
|
<div class="debug-player-header">${player.role} - ${player.name}</div>
|
||||||
|
<div class="debug-player-info">
|
||||||
|
<div class="debug-coords">Pos: (${player.position.x.toFixed(0)}, ${player.position.y.toFixed(0)})</div>
|
||||||
|
<div>Speed: <span class="debug-value debug-speed">${player.velocity.magnitude().toFixed(1)}</span></div>
|
||||||
|
<div>Energy: <span class="debug-value debug-energy">${player.state.energy.toFixed(0)}%</span></div>
|
||||||
|
<div>Has Puck: <span class="debug-value debug-puck">${player.state.hasPuck}</span></div>
|
||||||
|
<div>Behavior: <span class="debug-value debug-behavior">${player.aiState.behavior}</span></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Make the entire div clickable with better event handling
|
||||||
|
playerDiv.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.selectPlayer(player);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add mouse events for better feedback
|
||||||
|
playerDiv.addEventListener('mouseenter', (e) => {
|
||||||
|
if (!playerDiv.classList.contains('selected')) {
|
||||||
|
playerDiv.style.background = 'rgba(255, 255, 255, 0.15)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playerDiv.addEventListener('mouseleave', (e) => {
|
||||||
|
if (!playerDiv.classList.contains('selected')) {
|
||||||
|
playerDiv.style.background = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playerDiv.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
playerDiv.style.transform = 'scale(0.98)';
|
||||||
|
});
|
||||||
|
|
||||||
|
playerDiv.addEventListener('mouseup', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
playerDiv.style.transform = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(playerDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedPlayerInfo() {
|
||||||
|
if (!this.selectedPlayer) return;
|
||||||
|
|
||||||
|
const player = this.selectedPlayer;
|
||||||
|
const info = `
|
||||||
|
<div style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px;">
|
||||||
|
<h4 style="margin: 0 0 5px 0; color: #4a90e2;">${player.name} (${player.role})</h4>
|
||||||
|
<div style="color: #ccc; font-size: 11px;">Team: ${player.team.toUpperCase()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #ccc;">Position & Physics</h5>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Position:</span>
|
||||||
|
<span class="debug-attribute-value debug-coords">(${player.position.x.toFixed(1)}, ${player.position.y.toFixed(1)})</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Velocity:</span>
|
||||||
|
<span class="debug-attribute-value debug-coords">(${player.velocity.x.toFixed(1)}, ${player.velocity.y.toFixed(1)})</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Speed:</span>
|
||||||
|
<span class="debug-attribute-value">${player.velocity.magnitude().toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Target:</span>
|
||||||
|
<span class="debug-attribute-value debug-coords">(${player.targetPosition.x.toFixed(1)}, ${player.targetPosition.y.toFixed(1)})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #ccc;">Game State</h5>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Has Puck:</span>
|
||||||
|
<span class="debug-attribute-value">${player.state.hasPuck}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Energy:</span>
|
||||||
|
<span class="debug-attribute-value">${player.state.energy.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Checking:</span>
|
||||||
|
<span class="debug-attribute-value">${player.state.checking}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Injured:</span>
|
||||||
|
<span class="debug-attribute-value">${player.state.injured}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #ccc;">AI State</h5>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Behavior:</span>
|
||||||
|
<span class="debug-attribute-value">${player.aiState.behavior}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Target:</span>
|
||||||
|
<span class="debug-attribute-value">${player.aiState.target ? player.aiState.target.constructor.name : 'None'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Last Action:</span>
|
||||||
|
<span class="debug-attribute-value">${player.aiState.lastAction.toFixed(0)}ms ago</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Reaction Time:</span>
|
||||||
|
<span class="debug-attribute-value">${player.aiState.reactionTime.toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5 style="margin: 0 0 8px 0; color: #ccc;">Attributes</h5>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Speed:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.speed.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Shooting:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.shooting.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Passing:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.passing.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Defense:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.defense.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Checking:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.checking.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Puck Handling:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.puckHandling.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="debug-attribute">
|
||||||
|
<span class="debug-attribute-name">Awareness:</span>
|
||||||
|
<span class="debug-attribute-value">${player.attributes.awareness.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('debug-selected-player').innerHTML = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupGlobalDebugFunctions() {
|
||||||
|
// Add global debug helper functions to window object
|
||||||
|
window.debugHelpers = {
|
||||||
|
// Get all players
|
||||||
|
getPlayers: () => this.gameEngine.players,
|
||||||
|
|
||||||
|
// Get players by team
|
||||||
|
getHomeTeam: () => this.gameEngine.players.filter(p => p.team === 'home'),
|
||||||
|
getAwayTeam: () => this.gameEngine.players.filter(p => p.team === 'away'),
|
||||||
|
|
||||||
|
// Get players by position
|
||||||
|
getPlayersByPosition: (position) => this.gameEngine.players.filter(p => p.role === position),
|
||||||
|
getGoalies: () => this.gameEngine.players.filter(p => p.role === 'G'),
|
||||||
|
getDefense: () => this.gameEngine.players.filter(p => p.role === 'LD' || p.role === 'RD'),
|
||||||
|
getForwards: () => this.gameEngine.players.filter(p => ['LW', 'C', 'RW'].includes(p.role)),
|
||||||
|
|
||||||
|
// Get specific player
|
||||||
|
getPlayer: (id) => this.gameEngine.players.find(p => p.id === id),
|
||||||
|
|
||||||
|
// Get puck
|
||||||
|
getPuck: () => this.gameEngine.puck,
|
||||||
|
|
||||||
|
// Get game state
|
||||||
|
getGameState: () => this.gameEngine.gameState,
|
||||||
|
|
||||||
|
// Get player with puck
|
||||||
|
getPuckCarrier: () => this.gameEngine.players.find(p => p.state.hasPuck),
|
||||||
|
|
||||||
|
// Export game state for debugging
|
||||||
|
exportGameState: () => {
|
||||||
|
const state = {
|
||||||
|
gameState: this.gameEngine.gameState.getGameState(),
|
||||||
|
players: this.gameEngine.players.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
team: p.team,
|
||||||
|
role: p.role,
|
||||||
|
position: { x: p.position.x, y: p.position.y },
|
||||||
|
velocity: { x: p.velocity.x, y: p.velocity.y },
|
||||||
|
targetPosition: { x: p.targetPosition.x, y: p.targetPosition.y },
|
||||||
|
state: { ...p.state },
|
||||||
|
aiState: { ...p.aiState },
|
||||||
|
attributes: { ...p.attributes }
|
||||||
|
})),
|
||||||
|
puck: {
|
||||||
|
position: { x: this.gameEngine.puck.position.x, y: this.gameEngine.puck.position.y },
|
||||||
|
velocity: { x: this.gameEngine.puck.velocity.x, y: this.gameEngine.puck.velocity.y },
|
||||||
|
speed: this.gameEngine.puck.getSpeed(),
|
||||||
|
lastTouchedBy: this.gameEngine.puck.lastTouchedBy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log('Game State Export:', state);
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show debug panel
|
||||||
|
showDebug: () => this.showDebugPanel(),
|
||||||
|
|
||||||
|
// Hide debug panel
|
||||||
|
hideDebug: () => this.hideDebugPanel()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log available debug functions
|
||||||
|
console.log('Debug Helper Functions Available:');
|
||||||
|
console.log('- debugHelpers.getPlayers() - Get all players');
|
||||||
|
console.log('- debugHelpers.getHomeTeam() - Get home team players');
|
||||||
|
console.log('- debugHelpers.getAwayTeam() - Get away team players');
|
||||||
|
console.log('- debugHelpers.getPlayersByPosition(pos) - Get players by position');
|
||||||
|
console.log('- debugHelpers.getPlayer(id) - Get specific player by ID');
|
||||||
|
console.log('- debugHelpers.getPuck() - Get puck object');
|
||||||
|
console.log('- debugHelpers.getGameState() - Get game state');
|
||||||
|
console.log('- debugHelpers.getPuckCarrier() - Get player with puck');
|
||||||
|
console.log('- debugHelpers.exportGameState() - Export full game state');
|
||||||
|
console.log('- debugHelpers.showDebug() - Show debug panel');
|
||||||
|
console.log('- debugHelpers.hideDebug() - Hide debug panel');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,9 @@ class Renderer {
|
|||||||
constructor(canvas) {
|
constructor(canvas) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.ctx = canvas.getContext('2d');
|
this.ctx = canvas.getContext('2d');
|
||||||
this.camera = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
zoom: 1,
|
|
||||||
target: null,
|
|
||||||
smoothing: 0.1
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setupCanvas();
|
this.setupCanvas();
|
||||||
|
this.setupFixedCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCanvas() {
|
setupCanvas() {
|
||||||
@ -18,6 +12,27 @@ class Renderer {
|
|||||||
this.ctx.imageSmoothingEnabled = false;
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupFixedCamera() {
|
||||||
|
const rinkWidth = 1000;
|
||||||
|
const rinkHeight = 600;
|
||||||
|
const padding = 50;
|
||||||
|
|
||||||
|
const scaleX = this.canvas.width / (rinkWidth + padding * 2);
|
||||||
|
const scaleY = this.canvas.height / (rinkHeight + padding * 2);
|
||||||
|
const zoom = Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
|
const x = (this.canvas.width - rinkWidth * zoom) / 2;
|
||||||
|
const y = (this.canvas.height - rinkHeight * zoom) / 2;
|
||||||
|
|
||||||
|
this.camera = {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
zoom: zoom,
|
||||||
|
target: null,
|
||||||
|
smoothing: 0.1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
}
|
}
|
||||||
@ -142,9 +157,7 @@ class Renderer {
|
|||||||
this.applyCamera();
|
this.applyCamera();
|
||||||
|
|
||||||
players.forEach(player => {
|
players.forEach(player => {
|
||||||
if (player.state.penaltyTime <= 0) {
|
player.render(this.ctx);
|
||||||
player.render(this.ctx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
@ -160,7 +173,6 @@ class Renderer {
|
|||||||
drawUI(gameState) {
|
drawUI(gameState) {
|
||||||
this.updateScoreBoard(gameState);
|
this.updateScoreBoard(gameState);
|
||||||
this.updateGameStats(gameState);
|
this.updateGameStats(gameState);
|
||||||
this.updatePenalties(gameState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScoreBoard(gameState) {
|
updateScoreBoard(gameState) {
|
||||||
@ -175,27 +187,6 @@ class Renderer {
|
|||||||
document.getElementById('away-shots').textContent = gameState.stats.away.shots;
|
document.getElementById('away-shots').textContent = gameState.stats.away.shots;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePenalties(gameState) {
|
|
||||||
const homePenalties = document.getElementById('home-penalties');
|
|
||||||
const awayPenalties = document.getElementById('away-penalties');
|
|
||||||
|
|
||||||
homePenalties.innerHTML = '';
|
|
||||||
awayPenalties.innerHTML = '';
|
|
||||||
|
|
||||||
gameState.stats.home.penalties.forEach(penalty => {
|
|
||||||
const penaltyDiv = document.createElement('div');
|
|
||||||
penaltyDiv.className = 'penalty-box';
|
|
||||||
penaltyDiv.textContent = `${penalty.player}: ${penalty.type} (${Math.ceil(penalty.timeRemaining)}s)`;
|
|
||||||
homePenalties.appendChild(penaltyDiv);
|
|
||||||
});
|
|
||||||
|
|
||||||
gameState.stats.away.penalties.forEach(penalty => {
|
|
||||||
const penaltyDiv = document.createElement('div');
|
|
||||||
penaltyDiv.className = 'penalty-box';
|
|
||||||
penaltyDiv.textContent = `${penalty.player}: ${penalty.type} (${Math.ceil(penalty.timeRemaining)}s)`;
|
|
||||||
awayPenalties.appendChild(penaltyDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
drawParticleEffect(position, type, color = '#ffff00') {
|
drawParticleEffect(position, type, color = '#ffff00') {
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
@ -266,17 +257,7 @@ class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCamera(target) {
|
updateCamera(target) {
|
||||||
if (target) {
|
// Camera is now fixed - no updates needed
|
||||||
this.camera.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.camera.target) {
|
|
||||||
const targetX = this.canvas.width / 2 - this.camera.target.x * this.camera.zoom;
|
|
||||||
const targetY = this.canvas.height / 2 - this.camera.target.y * this.camera.zoom;
|
|
||||||
|
|
||||||
this.camera.x += (targetX - this.camera.x) * this.camera.smoothing;
|
|
||||||
this.camera.y += (targetY - this.camera.y) * this.camera.smoothing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCamera() {
|
applyCamera() {
|
||||||
@ -302,12 +283,22 @@ class Renderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawDebugInfo(gameState, players, puck) {
|
drawDebugInfo(gameState, players, puck, selectedPlayer = null) {
|
||||||
|
// Always draw selected player target line if there is one, even when debug mode is off
|
||||||
|
if (selectedPlayer) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.applyCamera();
|
||||||
|
this.drawSelectedPlayerTarget(selectedPlayer);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw the rest of debug info if debug mode is on
|
||||||
if (!window.debugMode) return;
|
if (!window.debugMode) return;
|
||||||
|
|
||||||
|
// Draw basic debug info overlay
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||||
this.ctx.fillRect(10, 10, 200, 150);
|
this.ctx.fillRect(10, 10, 250, 180);
|
||||||
|
|
||||||
this.ctx.fillStyle = '#fff';
|
this.ctx.fillStyle = '#fff';
|
||||||
this.ctx.font = '12px monospace';
|
this.ctx.font = '12px monospace';
|
||||||
@ -317,8 +308,219 @@ class Renderer {
|
|||||||
this.ctx.fillText(`Game Time: ${gameState.formatTime(gameState.timeRemaining)}`, 20, 90);
|
this.ctx.fillText(`Game Time: ${gameState.formatTime(gameState.timeRemaining)}`, 20, 90);
|
||||||
this.ctx.fillText(`Period: ${gameState.period}`, 20, 110);
|
this.ctx.fillText(`Period: ${gameState.period}`, 20, 110);
|
||||||
this.ctx.fillText(`Paused: ${gameState.isPaused}`, 20, 130);
|
this.ctx.fillText(`Paused: ${gameState.isPaused}`, 20, 130);
|
||||||
this.ctx.fillText(`Speed: ${gameState.gameSpeed}x`, 20, 150);
|
this.ctx.fillText(`Faceoff: ${gameState.faceoff.isActive}`, 20, 150);
|
||||||
|
this.ctx.fillText(`Puck Active: ${gameState.puckActive !== undefined ? gameState.puckActive : 'N/A'}`, 20, 170);
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
|
||||||
|
// Draw enhanced debug visualizations on the rink
|
||||||
|
this.ctx.save();
|
||||||
|
this.applyCamera();
|
||||||
|
|
||||||
|
this.drawDebugVectors(players, puck);
|
||||||
|
this.drawDebugPlayerInfo(players);
|
||||||
|
this.drawDebugPuckInfo(puck);
|
||||||
|
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawDebugVectors(players, puck) {
|
||||||
|
// Draw velocity vectors for players
|
||||||
|
players.forEach(player => {
|
||||||
|
if (player.velocity.magnitude() > 5) {
|
||||||
|
this.drawVector(player.position, player.velocity, '#ff4444', 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw target position
|
||||||
|
if (player.targetPosition) {
|
||||||
|
this.drawLine(player.position, player.targetPosition, '#44ff44', 1, [5, 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw AI target if exists
|
||||||
|
if (player.aiState.target && player.aiState.target.position) {
|
||||||
|
this.drawLine(player.position, player.aiState.target.position, '#ffff44', 1, [2, 2]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw puck velocity vector
|
||||||
|
if (puck.velocity.magnitude() > 10) {
|
||||||
|
this.drawVector(puck.position, puck.velocity, '#4444ff', 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawDebugPlayerInfo(players) {
|
||||||
|
this.ctx.font = '10px Arial';
|
||||||
|
|
||||||
|
players.forEach(player => {
|
||||||
|
const x = player.position.x;
|
||||||
|
const y = player.position.y - player.radius - 5;
|
||||||
|
|
||||||
|
// Draw player ID and energy
|
||||||
|
this.ctx.fillStyle = player.team === 'home' ? '#ff4444' : '#4444ff';
|
||||||
|
this.ctx.fillText(`${player.role}`, x - 10, y);
|
||||||
|
|
||||||
|
// Draw energy bar
|
||||||
|
const barWidth = 20;
|
||||||
|
const barHeight = 3;
|
||||||
|
const energyPercent = player.state.energy / 100;
|
||||||
|
|
||||||
|
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||||
|
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth, barHeight);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = energyPercent > 0.5 ? '#44ff44' : energyPercent > 0.25 ? '#ffff44' : '#ff4444';
|
||||||
|
this.ctx.fillRect(x - barWidth/2, y - 15, barWidth * energyPercent, barHeight);
|
||||||
|
|
||||||
|
// Highlight puck carrier
|
||||||
|
if (player.state.hasPuck) {
|
||||||
|
this.ctx.strokeStyle = '#ffff00';
|
||||||
|
this.ctx.lineWidth = 3;
|
||||||
|
this.ctx.setLineDash([3, 3]);
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(x, player.position.y, player.radius + 5, 0, Math.PI * 2);
|
||||||
|
this.ctx.stroke();
|
||||||
|
this.ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drawDebugPuckInfo(puck) {
|
||||||
|
const x = puck.position.x;
|
||||||
|
const y = puck.position.y - 20;
|
||||||
|
|
||||||
|
this.ctx.font = '8px Arial';
|
||||||
|
this.ctx.fillStyle = '#ffffff';
|
||||||
|
this.ctx.fillText(`Speed: ${Math.round(puck.velocity.magnitude())}`, x - 20, y);
|
||||||
|
|
||||||
|
if (puck.lastTouchedBy) {
|
||||||
|
this.ctx.fillText(`Last: ${puck.lastTouchedBy}`, x - 20, y + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawVector(position, vector, color = '#ffffff', scale = 1) {
|
||||||
|
const endX = position.x + vector.x * scale;
|
||||||
|
const endY = position.y + vector.y * scale;
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = color;
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Draw vector line
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(position.x, position.y);
|
||||||
|
this.ctx.lineTo(endX, endY);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Draw arrow head
|
||||||
|
const angle = Math.atan2(vector.y, vector.x);
|
||||||
|
const arrowSize = 8;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(endX, endY);
|
||||||
|
this.ctx.lineTo(
|
||||||
|
endX - arrowSize * Math.cos(angle - Math.PI / 6),
|
||||||
|
endY - arrowSize * Math.sin(angle - Math.PI / 6)
|
||||||
|
);
|
||||||
|
this.ctx.moveTo(endX, endY);
|
||||||
|
this.ctx.lineTo(
|
||||||
|
endX - arrowSize * Math.cos(angle + Math.PI / 6),
|
||||||
|
endY - arrowSize * Math.sin(angle + Math.PI / 6)
|
||||||
|
);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLine(start, end, color = '#ffffff', width = 1, dash = []) {
|
||||||
|
this.ctx.strokeStyle = color;
|
||||||
|
this.ctx.lineWidth = width;
|
||||||
|
this.ctx.setLineDash(dash);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(start.x, start.y);
|
||||||
|
this.ctx.lineTo(end.x, end.y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
this.ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSelectedPlayerTarget(selectedPlayer) {
|
||||||
|
if (!selectedPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedPlayer.targetPosition) {
|
||||||
|
console.log('Selected player has no target position:', selectedPlayer.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target is different from current position
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(selectedPlayer.targetPosition.x - selectedPlayer.position.x, 2) +
|
||||||
|
Math.pow(selectedPlayer.targetPosition.y - selectedPlayer.position.y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance < 5) {
|
||||||
|
// Target too close to player, not worth drawing line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current context state
|
||||||
|
this.ctx.save();
|
||||||
|
|
||||||
|
// Draw a bright, prominent line from selected player to their target
|
||||||
|
this.ctx.strokeStyle = '#00ffff'; // Cyan color for high visibility
|
||||||
|
this.ctx.lineWidth = 6;
|
||||||
|
this.ctx.setLineDash([12, 6]); // Larger dashed line pattern
|
||||||
|
this.ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y);
|
||||||
|
this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Also draw a solid white line underneath for extra visibility
|
||||||
|
this.ctx.strokeStyle = '#ffffff';
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.setLineDash([]);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(selectedPlayer.position.x, selectedPlayer.position.y);
|
||||||
|
this.ctx.lineTo(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Draw target position marker (circle)
|
||||||
|
this.ctx.setLineDash([]);
|
||||||
|
this.ctx.fillStyle = '#00ffff';
|
||||||
|
this.ctx.strokeStyle = '#ffffff';
|
||||||
|
this.ctx.lineWidth = 3;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(selectedPlayer.targetPosition.x, selectedPlayer.targetPosition.y, 10, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Draw crosshair in the target circle
|
||||||
|
this.ctx.strokeStyle = '#ffffff';
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
|
||||||
|
const targetX = selectedPlayer.targetPosition.x;
|
||||||
|
const targetY = selectedPlayer.targetPosition.y;
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(targetX - 6, targetY);
|
||||||
|
this.ctx.lineTo(targetX + 6, targetY);
|
||||||
|
this.ctx.moveTo(targetX, targetY - 6);
|
||||||
|
this.ctx.lineTo(targetX, targetY + 6);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Highlight the selected player with a special border
|
||||||
|
this.ctx.strokeStyle = '#00ffff';
|
||||||
|
this.ctx.lineWidth = 4;
|
||||||
|
this.ctx.setLineDash([8, 4]);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(selectedPlayer.position.x, selectedPlayer.position.y, selectedPlayer.radius + 10, 0, Math.PI * 2);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// Restore context state
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -3,15 +3,11 @@ class RulesSystem {
|
|||||||
this.gameState = gameState;
|
this.gameState = gameState;
|
||||||
this.lastOffsideCheck = 0;
|
this.lastOffsideCheck = 0;
|
||||||
this.lastIcingCheck = 0;
|
this.lastIcingCheck = 0;
|
||||||
this.penaltyQueue = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(players, puck, deltaTime) {
|
update(players, puck, deltaTime) {
|
||||||
this.checkOffside(players, puck);
|
this.checkOffside(players, puck);
|
||||||
this.checkIcing(players, puck);
|
this.checkIcing(players, puck);
|
||||||
this.checkGoaltenderInterference(players, puck);
|
|
||||||
this.checkHighSticking(players, puck);
|
|
||||||
this.processPenaltyQueue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOffside(players, puck) {
|
checkOffside(players, puck) {
|
||||||
@ -126,81 +122,6 @@ class RulesSystem {
|
|||||||
this.scheduleFaceoff(faceoffPosition);
|
this.scheduleFaceoff(faceoffPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkGoaltenderInterference(players, puck) {
|
|
||||||
const goalies = players.filter(p => p.role === 'G');
|
|
||||||
|
|
||||||
goalies.forEach(goalie => {
|
|
||||||
const crease = this.getCreaseArea(goalie.team);
|
|
||||||
const opponents = players.filter(p =>
|
|
||||||
p.team !== goalie.team &&
|
|
||||||
this.isPlayerInCrease(p, crease)
|
|
||||||
);
|
|
||||||
|
|
||||||
opponents.forEach(opponent => {
|
|
||||||
if (opponent.velocity.magnitude() > 100) {
|
|
||||||
this.callGoaltenderInterference(opponent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkHighSticking(players, puck) {
|
|
||||||
if (puck.position.y < 200) {
|
|
||||||
const lastTouch = puck.lastPlayerTouch;
|
|
||||||
if (lastTouch && Math.random() < 0.1) {
|
|
||||||
this.callHighSticking(lastTouch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlayerInCrease(player, crease) {
|
|
||||||
return player.position.x >= crease.x &&
|
|
||||||
player.position.x <= crease.x + crease.width &&
|
|
||||||
player.position.y >= crease.y &&
|
|
||||||
player.position.y <= crease.y + crease.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCreaseArea(team) {
|
|
||||||
const goalY = this.gameState.rink.centerY;
|
|
||||||
const goalX = team === 'home' ? 50 : this.gameState.rink.width - 50;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: goalX - 30,
|
|
||||||
y: goalY - 60,
|
|
||||||
width: 60,
|
|
||||||
height: 120
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
callGoaltenderInterference(player) {
|
|
||||||
this.queuePenalty(player.team, player.name, 'Goaltender Interference', 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
callHighSticking(player) {
|
|
||||||
this.queuePenalty(player.team, player.name, 'High Sticking', 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
queuePenalty(team, player, type, duration) {
|
|
||||||
this.penaltyQueue.push({
|
|
||||||
team,
|
|
||||||
player,
|
|
||||||
type,
|
|
||||||
duration,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
processPenaltyQueue() {
|
|
||||||
if (this.penaltyQueue.length === 0) return;
|
|
||||||
|
|
||||||
const penalty = this.penaltyQueue.shift();
|
|
||||||
this.gameState.addPenalty(
|
|
||||||
penalty.team,
|
|
||||||
penalty.player,
|
|
||||||
penalty.type,
|
|
||||||
penalty.duration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleFaceoff(position) {
|
scheduleFaceoff(position) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -223,34 +144,8 @@ class RulesSystem {
|
|||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkFighting(players) {
|
|
||||||
for (let i = 0; i < players.length; i++) {
|
|
||||||
for (let j = i + 1; j < players.length; j++) {
|
|
||||||
const player1 = players[i];
|
|
||||||
const player2 = players[j];
|
|
||||||
|
|
||||||
if (player1.team !== player2.team &&
|
|
||||||
player1.position.distance(player2.position) < 20 &&
|
|
||||||
player1.velocity.magnitude() > 150 &&
|
|
||||||
player2.velocity.magnitude() > 150 &&
|
|
||||||
Math.random() < 0.01) {
|
|
||||||
|
|
||||||
this.callFighting(player1, player2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callFighting(player1, player2) {
|
|
||||||
this.queuePenalty(player1.team, player1.name, 'Fighting', 300);
|
|
||||||
this.queuePenalty(player2.team, player2.name, 'Fighting', 300);
|
|
||||||
|
|
||||||
this.gameState.addEvent(`FIGHT - ${player1.name} vs ${player2.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.lastOffsideCheck = 0;
|
this.lastOffsideCheck = 0;
|
||||||
this.lastIcingCheck = 0;
|
this.lastIcingCheck = 0;
|
||||||
this.penaltyQueue = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user