Compare commits

...

10 Commits

Author SHA1 Message Date
Pierre Wessman
cad05ad895 increase player speed 2025-09-16 16:48:41 +02:00
Pierre Wessman
cfdad79207 aggressive pressure 2025-09-16 15:13:06 +02:00
Pierre Wessman
326b3036b6 puck handling 2025-09-16 14:48:30 +02:00
Pierre Wessman
c0ced95d46 bugfix: has puck 2025-09-16 14:31:07 +02:00
Pierre Wessman
ac5b83afee debug system 2025-09-16 14:24:09 +02:00
Pierre Wessman
c09bc6edd5 function comments 2025-09-16 13:58:13 +02:00
Pierre Wessman
4236d0098d ... 2025-09-16 13:33:01 +02:00
Pierre Wessman
2533e3c50e stronger passes 2025-09-16 13:21:58 +02:00
Pierre Wessman
344569edee fix camera 2025-09-16 13:11:40 +02:00
Pierre Wessman
abe1e2ca54 remove penalties 2025-09-16 13:08:41 +02:00
13 changed files with 1395 additions and 251 deletions

View File

@ -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
View 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.

View File

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

View File

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

View File

@ -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">&times;</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>

View File

@ -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;
}); });

View File

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

View File

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

View File

@ -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);
} }

View File

@ -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
View 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');
}
}

View File

@ -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();
}
} }

View File

@ -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 = [];
} }
} }