commit 0eb0574fbd269bf1dc55ee31ad43ba70d1a03792
Author: Pierre Wessman <4029607+pierrewessman@users.noreply.github.com>
Date: Tue Sep 16 10:30:13 2025 +0200
init
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..2908062
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,9 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7a0718a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# Hockey Manager - 2D Match Engine
+
+A JavaScript-based 2D hockey match engine for web browsers featuring realistic physics, AI players, and complete hockey rules implementation.
+
+## Features
+
+### Core Game Engine
+- **60 FPS game loop** with requestAnimationFrame
+- **Physics system** with collision detection and realistic puck/player movement
+- **Game state management** with time tracking, scoring, and statistics
+- **Event-driven architecture** for game events and interactions
+
+### Gameplay Features
+- **12 players** (6 per team) with distinct roles (C, LW, RW, LD, RD, G)
+- **Realistic hockey rules**: offsides, icing, penalties, power plays
+- **Player AI behaviors**: puck handling, passing, shooting, checking, formations
+- **Complete hockey rink** with proper dimensions, goals, creases, and faceoff dots
+- **Live statistics** tracking shots, saves, hits, penalties, ice time
+
+### Visual Features
+- **2D Canvas rendering** with smooth animations
+- **Hockey rink visualization** with proper lines, zones, and markings
+- **Player sprites** with team colors and role indicators
+- **Puck physics** with trail effects and realistic bouncing
+- **Camera system** with zoom and follow capabilities
+- **Particle effects** for goals, saves, and collisions
+
+### Controls & UI
+- **Real-time scoreboard** with period and clock display
+- **Game statistics panel** showing shots, saves, and penalties
+- **Interactive controls** for play/pause, speed adjustment, and reset
+- **Keyboard shortcuts** for quick game control
+- **Responsive design** that adapts to screen size
+
+## Project Structure
+
+```
+hockey-manager/
+├── index.html # Main HTML entry point
+├── css/
+│ └── styles.css # Game styling and UI
+├── src/
+│ ├── engine/
+│ │ ├── game-engine.js # Main game engine
+│ │ ├── game-state.js # Game state management
+│ │ └── main.js # Application initialization
+│ ├── entities/
+│ │ ├── player.js # Player entity with AI
+│ │ └── puck.js # Puck physics and behavior
+│ ├── systems/
+│ │ ├── renderer.js # 2D rendering system
+│ │ ├── physics-system.js # Physics calculations
+│ │ ├── ai-system.js # AI formation and strategy
+│ │ └── rules-system.js # Hockey rules enforcement
+│ └── utils/
+│ ├── vector.js # 2D vector mathematics
+│ └── physics.js # Physics utility functions
+```
+
+## Getting Started
+
+1. **Open the game**: Simply open `index.html` in a modern web browser
+2. **Start playing**: The game will automatically initialize and start
+3. **Use controls**:
+ - **Space**: Pause/Resume
+ - **D**: Toggle debug mode
+ - **R**: Reset game
+ - **Mouse wheel**: Zoom in/out
+ - **F11**: Toggle fullscreen
+
+## Controls Reference
+
+### Keyboard Controls
+- `SPACE` - Pause/Resume game
+- `D` - Toggle debug information display
+- `R` - Reset game to initial state
+- `F11` - Toggle fullscreen mode
+
+### Mouse Controls
+- `Mouse Wheel` - Zoom camera in/out
+- `Click buttons` - Use UI controls for game management
+
+### UI Controls
+- **Play/Pause** - Start/stop game simulation
+- **Speed Control** - Adjust game speed (0.5x, 1x, 2x, 4x)
+- **Reset Game** - Return to initial game state
+
+## Game Mechanics
+
+### Player AI
+Each player has sophisticated AI that includes:
+- **Role-based behavior** (forwards vs defensemen vs goalie)
+- **Formation positioning** based on game situation
+- **Puck awareness** and decision making
+- **Teammate cooperation** for passing and positioning
+- **Opponent pressure** and defensive reactions
+
+### Physics System
+- **Realistic puck physics** with momentum and friction
+- **Player collision detection** and response
+- **Board bouncing** with energy conservation
+- **Goalie save mechanics** based on skill attributes
+
+### Hockey Rules
+- **Offside detection** when players cross lines ahead of puck
+- **Icing calls** for long shots across multiple zones
+- **Penalty system** for infractions like checking and interference
+- **Power play situations** with player advantages
+- **Faceoff mechanics** at center ice and zone dots
+
+## Technical Details
+
+### Performance
+- Optimized 60 FPS rendering
+- Efficient collision detection
+- Smooth camera interpolation
+- Responsive UI updates
+
+### Browser Compatibility
+- Modern browsers with HTML5 Canvas support
+- Chrome, Firefox, Safari, Edge
+- Mobile browsers (with touch controls)
+
+### Customization
+The engine is highly modular and can be extended with:
+- Custom player attributes and skills
+- Different team formations and strategies
+- Additional hockey rules and penalties
+- Enhanced visual effects and animations
+- Sound effects and music integration
+
+## Development
+
+The codebase is organized into clean, modular components:
+- **Entity-Component system** for game objects
+- **Event-driven communication** between systems
+- **Separation of concerns** (rendering, physics, AI, rules)
+- **Utility libraries** for common operations
+
+## Future Enhancements
+
+Potential improvements include:
+- **Sound system** with hockey arena audio
+- **Advanced AI** with machine learning behaviors
+- **Multiplayer support** for online matches
+- **Tournament mode** with brackets and playoffs
+- **Player statistics** tracking across multiple games
+- **Save/load functionality** for game states
+
+---
+
+*This 2D hockey match engine provides a complete foundation for hockey management games, featuring realistic gameplay mechanics and professional-quality code architecture.*
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
new file mode 100644
index 0000000..247b78a
--- /dev/null
+++ b/css/styles.css
@@ -0,0 +1,132 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Arial', sans-serif;
+ background: #1a1a1a;
+ color: white;
+ overflow: hidden;
+}
+
+#game-container {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+#game-canvas {
+ border: 2px solid #333;
+ background: #2a2a2a;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+}
+
+#game-ui {
+ position: absolute;
+ top: 20px;
+ width: 100%;
+ z-index: 10;
+ pointer-events: none;
+}
+
+#score-board {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0, 0, 0, 0.8);
+ padding: 15px 30px;
+ border-radius: 10px;
+ margin: 0 auto;
+ width: fit-content;
+ gap: 40px;
+}
+
+.team {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+}
+
+.team-name {
+ font-size: 14px;
+ font-weight: bold;
+ color: #ccc;
+}
+
+.score {
+ font-size: 36px;
+ font-weight: bold;
+ color: #fff;
+}
+
+.time-display {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+}
+
+#period {
+ font-size: 14px;
+ color: #ccc;
+}
+
+#clock {
+ font-size: 24px;
+ font-weight: bold;
+ color: #fff;
+}
+
+#game-stats {
+ position: absolute;
+ top: 100px;
+ left: 20px;
+ background: rgba(0, 0, 0, 0.8);
+ padding: 15px;
+ border-radius: 10px;
+ font-size: 14px;
+}
+
+#penalties {
+ margin-top: 10px;
+}
+
+#controls {
+ position: absolute;
+ bottom: 20px;
+ display: flex;
+ gap: 10px;
+ pointer-events: auto;
+}
+
+button {
+ padding: 10px 20px;
+ background: #4a90e2;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: bold;
+ transition: background 0.3s;
+}
+
+button:hover {
+ background: #357abd;
+}
+
+button:active {
+ transform: translateY(1px);
+}
+
+.penalty-box {
+ background: #ff4444;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 3px;
+ margin: 2px;
+ font-size: 12px;
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..ea2b1c7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ Hockey Manager - 2D Match Engine
+
+
+
+
+
+
+
+
+ Home
+ 0
+
+
+ 1st
+ 20:00
+
+
+ Away
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/engine/game-engine.js b/src/engine/game-engine.js
new file mode 100644
index 0000000..d9795fe
--- /dev/null
+++ b/src/engine/game-engine.js
@@ -0,0 +1,350 @@
+class GameEngine {
+ constructor(canvas) {
+ this.canvas = canvas;
+ this.gameState = new GameState();
+ this.renderer = new Renderer(canvas);
+ this.audioSystem = new AudioSystem();
+
+ this.players = [];
+ this.puck = new Puck(500, 300);
+
+ this.lastTime = 0;
+ this.deltaTime = 0;
+ this.targetFPS = 60;
+ this.frameTime = 1000 / this.targetFPS;
+
+ this.isRunning = false;
+ this.effects = [];
+
+ this.setupPlayers();
+ this.setupEventListeners();
+ this.setupControls();
+ }
+
+ setupPlayers() {
+ const homeTeamPositions = [
+ { role: 'G', x: 80, y: 300 },
+ { role: 'LD', x: 200, y: 220 },
+ { role: 'RD', x: 200, y: 380 },
+ { role: 'LW', x: 350, y: 200 },
+ { role: 'C', x: 400, y: 300 },
+ { role: 'RW', x: 350, y: 400 }
+ ];
+
+ const awayTeamPositions = [
+ { role: 'G', x: 920, y: 300 },
+ { role: 'LD', x: 800, y: 220 },
+ { role: 'RD', x: 800, y: 380 },
+ { role: 'LW', x: 650, y: 200 },
+ { role: 'C', x: 600, y: 300 },
+ { role: 'RW', x: 650, y: 400 }
+ ];
+
+ homeTeamPositions.forEach((pos, index) => {
+ const player = new Player(
+ `home_${index}`,
+ `Player ${index + 1}`,
+ 'home',
+ pos.role,
+ pos.x,
+ pos.y
+ );
+ this.players.push(player);
+ });
+
+ awayTeamPositions.forEach((pos, index) => {
+ const player = new Player(
+ `away_${index}`,
+ `Player ${index + 7}`,
+ 'away',
+ pos.role,
+ pos.x,
+ pos.y
+ );
+ this.players.push(player);
+ });
+ }
+
+ setupEventListeners() {
+ this.gameState.on('goal', (data) => {
+ this.addEffect({
+ type: 'goal',
+ position: this.puck.position.copy(),
+ duration: 2000,
+ startTime: Date.now()
+ });
+
+ this.audioSystem.onGoal(data.team);
+
+ setTimeout(() => {
+ this.startFaceoff();
+ }, 2000);
+ });
+
+ this.gameState.on('save', (data) => {
+ this.addEffect({
+ type: 'save',
+ position: this.puck.position.copy(),
+ duration: 1000,
+ startTime: Date.now()
+ });
+
+ this.audioSystem.onSave();
+ });
+
+ this.gameState.on('penalty', (data) => {
+ const penalizedPlayer = this.players.find(p => p.name === data.player);
+ if (penalizedPlayer) {
+ penalizedPlayer.state.penaltyTime = data.duration;
+ }
+
+ this.audioSystem.onPenalty();
+ });
+
+ this.gameState.on('period-end', () => {
+ this.audioSystem.onPeriodEnd();
+ });
+ }
+
+ setupControls() {
+ document.getElementById('play-pause').addEventListener('click', () => {
+ this.gameState.togglePause();
+ });
+
+ document.getElementById('speed-control').addEventListener('click', (e) => {
+ const speeds = [0.5, 1, 2, 4];
+ const currentIndex = speeds.indexOf(this.gameState.gameSpeed);
+ const nextIndex = (currentIndex + 1) % speeds.length;
+ this.gameState.setSpeed(speeds[nextIndex]);
+ e.target.textContent = `Speed: ${speeds[nextIndex]}x`;
+ });
+
+ document.getElementById('sound-toggle').addEventListener('click', (e) => {
+ const enabled = this.audioSystem.toggle();
+ e.target.textContent = `Sound: ${enabled ? 'ON' : 'OFF'}`;
+ });
+
+ document.getElementById('reset-game').addEventListener('click', () => {
+ this.resetGame();
+ });
+
+ window.addEventListener('keydown', (e) => {
+ switch (e.key) {
+ case ' ':
+ e.preventDefault();
+ this.gameState.togglePause();
+ break;
+ case 'd':
+ window.debugMode = !window.debugMode;
+ break;
+ case 'r':
+ this.resetGame();
+ break;
+ }
+ });
+
+ this.canvas.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
+ this.renderer.setZoom(this.renderer.camera.zoom * zoomFactor);
+ });
+ }
+
+ start() {
+ this.isRunning = true;
+ this.lastTime = performance.now();
+ this.gameLoop();
+ }
+
+ stop() {
+ this.isRunning = false;
+ }
+
+ gameLoop(currentTime = performance.now()) {
+ if (!this.isRunning) return;
+
+ this.deltaTime = (currentTime - this.lastTime) / 1000;
+ this.lastTime = currentTime;
+
+ this.deltaTime = Math.min(this.deltaTime, 1/30);
+
+ this.update(this.deltaTime);
+ this.render();
+
+ requestAnimationFrame((time) => this.gameLoop(time));
+ }
+
+ update(deltaTime) {
+ if (this.gameState.isPaused) return;
+
+ this.gameState.updateTime(deltaTime);
+
+ this.players.forEach(player => {
+ player.update(deltaTime, this.gameState, this.puck, this.players);
+ });
+
+ this.puck.update(deltaTime, this.gameState, this.players);
+
+ this.updateCollisions();
+ this.updateEffects(deltaTime);
+
+ this.renderer.updateCamera(this.puck.position);
+ }
+
+ updateCollisions() {
+ for (let i = 0; i < this.players.length; i++) {
+ for (let j = i + 1; j < this.players.length; j++) {
+ const player1 = this.players[i];
+ const player2 = this.players[j];
+
+ if (Physics.checkCircleCollision(
+ player1.position, player1.radius,
+ player2.position, player2.radius
+ )) {
+ Physics.resolveCircleCollision(player1, player2);
+
+ if (player1.team !== player2.team &&
+ (player1.velocity.magnitude() > 100 || player2.velocity.magnitude() > 100)) {
+ this.handlePlayerCollision(player1, player2);
+ }
+ }
+ }
+ }
+ }
+
+ handlePlayerCollision(player1, player2) {
+ const speed1 = player1.velocity.magnitude();
+ const speed2 = player2.velocity.magnitude();
+
+ if (speed1 > 150 || speed2 > 150) {
+ this.addEffect({
+ type: 'hit',
+ position: player1.position.lerp(player2.position, 0.5),
+ duration: 500,
+ startTime: Date.now()
+ });
+
+ if (Math.random() < 0.1) {
+ const penalizedPlayer = speed1 > speed2 ? player1 : player2;
+ this.gameState.addPenalty(
+ penalizedPlayer.team,
+ penalizedPlayer.name,
+ 'Checking',
+ 120
+ );
+ }
+ }
+ }
+
+ render() {
+ this.renderer.clear();
+ this.renderer.drawRink(this.gameState);
+ this.renderer.drawPlayers(this.players);
+ this.renderer.drawPuck(this.puck);
+ this.renderEffects();
+ this.renderer.drawUI(this.gameState);
+ this.renderer.drawDebugInfo(this.gameState, this.players, this.puck);
+ }
+
+ renderEffects() {
+ this.effects.forEach(effect => {
+ const elapsed = Date.now() - effect.startTime;
+ const progress = elapsed / effect.duration;
+
+ if (progress < 1) {
+ this.renderer.drawParticleEffect(effect.position, effect.type);
+ }
+ });
+ }
+
+ updateEffects(deltaTime) {
+ this.effects = this.effects.filter(effect => {
+ const elapsed = Date.now() - effect.startTime;
+ return elapsed < effect.duration;
+ });
+ }
+
+ addEffect(effect) {
+ this.effects.push(effect);
+ }
+
+ startFaceoff(position = null) {
+ if (!position) {
+ position = new Vector2(this.gameState.rink.centerX, this.gameState.rink.centerY);
+ }
+
+ this.puck.reset(position.x, position.y);
+
+ const nearbyPlayers = this.players.filter(player =>
+ player.position.distance(position) < 100
+ ).sort((a, b) =>
+ a.position.distance(position) - b.position.distance(position)
+ );
+
+ if (nearbyPlayers.length >= 2) {
+ const player1 = nearbyPlayers[0];
+ const player2 = nearbyPlayers[1];
+
+ // Immediately start the faceoff
+ this.puck.faceoff(player1, player2);
+ } else {
+ // If no players nearby, give puck to closest player
+ const allPlayers = this.players.filter(p => p.role !== 'G');
+ if (allPlayers.length > 0) {
+ const closest = allPlayers.reduce((closest, player) =>
+ player.position.distance(position) < closest.position.distance(position) ? player : closest
+ );
+ closest.state.hasPuck = true;
+ this.puck.lastPlayerTouch = closest;
+ this.puck.lastTeamTouch = closest.team;
+ }
+ }
+ }
+
+ resetGame() {
+ this.gameState.reset();
+ this.effects = [];
+
+ // Clear all player states first
+ this.players.forEach(player => {
+ player.position = player.homePosition.copy();
+ player.velocity = new Vector2(0, 0);
+ player.state.hasPuck = false;
+ player.state.energy = 100;
+ player.state.penaltyTime = 0;
+ player.aiState.lastAction = 0;
+ });
+
+ // Reset puck after players
+ this.puck.reset();
+
+ // Start faceoff after a short delay
+ setTimeout(() => {
+ this.startFaceoff();
+ }, 1000);
+ }
+
+ getGameData() {
+ return {
+ gameState: this.gameState.getGameState(),
+ players: this.players.map(p => ({
+ id: p.id,
+ name: p.name,
+ team: p.team,
+ position: p.position,
+ role: p.role,
+ state: p.state
+ })),
+ puck: {
+ position: this.puck.position,
+ velocity: this.puck.velocity,
+ speed: this.puck.getSpeed()
+ }
+ };
+ }
+
+ loadGameData(data) {
+ // Implementation for loading saved game state
+ // This would restore the game from a saved state
+ }
+}
\ No newline at end of file
diff --git a/src/engine/game-state.js b/src/engine/game-state.js
new file mode 100644
index 0000000..3f20575
--- /dev/null
+++ b/src/engine/game-state.js
@@ -0,0 +1,208 @@
+class GameState {
+ constructor() {
+ this.reset();
+ this.eventListeners = {};
+ }
+
+ reset() {
+ this.period = 1;
+ this.timeRemaining = 20 * 60; // 20 minutes in seconds
+ this.homeScore = 0;
+ this.awayScore = 0;
+ this.gameSpeed = 1;
+ this.isPaused = false;
+ this.gameOver = false;
+
+ this.stats = {
+ home: {
+ shots: 0,
+ saves: 0,
+ hits: 0,
+ penalties: [],
+ faceoffWins: 0
+ },
+ away: {
+ shots: 0,
+ saves: 0,
+ hits: 0,
+ penalties: [],
+ faceoffWins: 0
+ }
+ };
+
+ this.gameEvents = [];
+ this.lastEventTime = 0;
+
+ this.rink = {
+ width: 1000,
+ height: 600,
+ centerX: 500,
+ centerY: 300,
+ goalWidth: 120,
+ goalHeight: 40,
+ corners: {
+ radius: 80
+ },
+ faceoffDots: [
+ { x: 200, y: 150 }, // Home left
+ { x: 200, y: 450 }, // Home right
+ { x: 500, y: 300 }, // Center
+ { x: 800, y: 150 }, // Away left
+ { x: 800, y: 450 } // Away right
+ ]
+ };
+
+ this.powerPlay = {
+ home: null,
+ away: null
+ };
+ }
+
+ formatTime(seconds) {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
+ }
+
+ getPeriodName() {
+ switch (this.period) {
+ case 1: return '1st';
+ case 2: return '2nd';
+ case 3: return '3rd';
+ default: return 'OT';
+ }
+ }
+
+ updateTime(deltaTime) {
+ if (this.isPaused || this.gameOver) return;
+
+ this.timeRemaining -= deltaTime * this.gameSpeed;
+
+ if (this.timeRemaining <= 0) {
+ this.timeRemaining = 0;
+ this.endPeriod();
+ }
+
+ this.updatePenalties(deltaTime);
+ }
+
+ endPeriod() {
+ this.period++;
+ if (this.period > 3 && this.homeScore !== this.awayScore) {
+ this.gameOver = true;
+ this.emit('game-end', { winner: this.homeScore > this.awayScore ? 'home' : 'away' });
+ } else if (this.period <= 3) {
+ this.timeRemaining = 20 * 60;
+ this.emit('period-end', { period: this.period - 1 });
+ } else {
+ this.timeRemaining = 5 * 60; // Overtime
+ this.emit('overtime-start');
+ }
+ }
+
+ addGoal(team) {
+ if (team === 'home') {
+ this.homeScore++;
+ } else {
+ this.awayScore++;
+ }
+ this.addEvent(`GOAL - ${team.toUpperCase()}!`);
+ this.emit('goal', { team, homeScore: this.homeScore, awayScore: this.awayScore });
+ }
+
+ addShot(team) {
+ this.stats[team].shots++;
+ this.emit('shot', { team, shots: this.stats[team].shots });
+ }
+
+ addSave(team) {
+ 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) {
+ const event = {
+ time: this.formatTime(20 * 60 - this.timeRemaining),
+ period: this.period,
+ description,
+ timestamp: Date.now()
+ };
+ this.gameEvents.unshift(event);
+ if (this.gameEvents.length > 50) {
+ this.gameEvents.pop();
+ }
+ }
+
+ togglePause() {
+ this.isPaused = !this.isPaused;
+ this.emit('pause-toggle', { isPaused: this.isPaused });
+ }
+
+ setSpeed(speed) {
+ this.gameSpeed = Math.max(0.1, Math.min(5, speed));
+ this.emit('speed-change', { speed: this.gameSpeed });
+ }
+
+ on(event, callback) {
+ if (!this.eventListeners[event]) {
+ this.eventListeners[event] = [];
+ }
+ this.eventListeners[event].push(callback);
+ }
+
+ emit(event, data) {
+ if (this.eventListeners[event]) {
+ this.eventListeners[event].forEach(callback => callback(data));
+ }
+ }
+
+ getGameState() {
+ return {
+ period: this.period,
+ timeRemaining: this.timeRemaining,
+ homeScore: this.homeScore,
+ awayScore: this.awayScore,
+ stats: { ...this.stats },
+ isPaused: this.isPaused,
+ gameSpeed: this.gameSpeed,
+ gameOver: this.gameOver,
+ powerPlay: { ...this.powerPlay }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/engine/main.js b/src/engine/main.js
new file mode 100644
index 0000000..1224c71
--- /dev/null
+++ b/src/engine/main.js
@@ -0,0 +1,163 @@
+class HockeyManager {
+ constructor() {
+ this.gameEngine = null;
+ this.canvas = null;
+ this.initialized = false;
+ }
+
+ async init() {
+ if (this.initialized) return;
+
+ try {
+ this.canvas = document.getElementById('game-canvas');
+ if (!this.canvas) {
+ throw new Error('Canvas element not found');
+ }
+
+ this.setupCanvas();
+ this.gameEngine = new GameEngine(this.canvas);
+ this.setupGlobalEvents();
+
+ console.log('Hockey Manager initialized successfully');
+ this.initialized = true;
+
+ } catch (error) {
+ console.error('Failed to initialize Hockey Manager:', error);
+ this.showError('Failed to initialize game: ' + error.message);
+ }
+ }
+
+ setupCanvas() {
+ const container = document.getElementById('game-container');
+ const containerRect = container.getBoundingClientRect();
+
+ this.canvas.width = Math.min(1200, containerRect.width - 40);
+ this.canvas.height = Math.min(800, containerRect.height - 200);
+
+ this.canvas.style.width = this.canvas.width + 'px';
+ this.canvas.style.height = this.canvas.height + 'px';
+ }
+
+ setupGlobalEvents() {
+ window.addEventListener('resize', () => {
+ this.handleResize();
+ });
+
+ window.addEventListener('beforeunload', () => {
+ this.cleanup();
+ });
+
+ document.addEventListener('visibilitychange', () => {
+ if (document.hidden) {
+ this.gameEngine.gameState.isPaused = true;
+ }
+ });
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'F11') {
+ e.preventDefault();
+ this.toggleFullscreen();
+ }
+ });
+ }
+
+ handleResize() {
+ if (!this.initialized) return;
+
+ clearTimeout(this.resizeTimeout);
+ this.resizeTimeout = setTimeout(() => {
+ this.setupCanvas();
+ }, 250);
+ }
+
+ toggleFullscreen() {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen().catch(err => {
+ console.warn('Could not enable fullscreen:', err);
+ });
+ } else {
+ document.exitFullscreen();
+ }
+ }
+
+ start() {
+ if (!this.initialized) {
+ console.error('Game not initialized. Call init() first.');
+ return;
+ }
+
+ this.gameEngine.start();
+ console.log('Hockey Manager started');
+ }
+
+ stop() {
+ if (this.gameEngine) {
+ this.gameEngine.stop();
+ console.log('Hockey Manager stopped');
+ }
+ }
+
+ cleanup() {
+ this.stop();
+ // Clean up any resources, event listeners, etc.
+ }
+
+ showError(message) {
+ const errorDiv = document.createElement('div');
+ errorDiv.style.cssText = `
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: #ff4444;
+ color: white;
+ padding: 20px;
+ border-radius: 10px;
+ z-index: 1000;
+ font-family: Arial, sans-serif;
+ `;
+ errorDiv.textContent = message;
+ document.body.appendChild(errorDiv);
+
+ setTimeout(() => {
+ if (errorDiv.parentNode) {
+ errorDiv.parentNode.removeChild(errorDiv);
+ }
+ }, 5000);
+ }
+
+ getGameEngine() {
+ return this.gameEngine;
+ }
+}
+
+let hockeyManager;
+let players;
+
+document.addEventListener('DOMContentLoaded', async () => {
+ try {
+ hockeyManager = new HockeyManager();
+ await hockeyManager.init();
+ hockeyManager.start();
+
+ players = hockeyManager.gameEngine.players;
+
+ // Initial faceoff to start the game
+ setTimeout(() => {
+ hockeyManager.gameEngine.startFaceoff();
+ }, 2000);
+
+ console.log('Hockey Manager 2D Match Engine loaded successfully!');
+ console.log('Controls:');
+ console.log('- SPACE: Pause/Resume');
+ console.log('- D: Toggle debug mode');
+ console.log('- R: Reset game');
+ console.log('- Mouse wheel: Zoom in/out');
+ console.log('- F11: Toggle fullscreen');
+
+ } catch (error) {
+ console.error('Failed to start Hockey Manager:', error);
+ }
+});
+
+window.hockeyManager = hockeyManager;
\ No newline at end of file
diff --git a/src/entities/player.js b/src/entities/player.js
new file mode 100644
index 0000000..7e28349
--- /dev/null
+++ b/src/entities/player.js
@@ -0,0 +1,355 @@
+class Player {
+ constructor(id, name, team, position, x, y) {
+ this.id = id;
+ this.name = name;
+ this.team = team; // 'home' or 'away'
+ this.position = new Vector2(x, y);
+ this.velocity = new Vector2(0, 0);
+ this.targetPosition = new Vector2(x, y);
+
+ this.role = position; // 'LW', 'C', 'RW', 'LD', 'RD', 'G'
+ this.radius = position === 'G' ? 20 : 12;
+ this.mass = position === 'G' ? 2 : 1;
+ this.maxSpeed = position === 'G' ? 150 : 200;
+ this.acceleration = 800;
+ this.restitution = 0.8;
+
+ this.attributes = {
+ speed: Math.random() * 20 + 70,
+ shooting: Math.random() * 20 + 70,
+ passing: Math.random() * 20 + 70,
+ defense: Math.random() * 20 + 70,
+ checking: Math.random() * 20 + 70,
+ puckHandling: Math.random() * 20 + 70,
+ awareness: Math.random() * 20 + 70
+ };
+
+ this.state = {
+ hasPuck: false,
+ energy: 100,
+ checking: false,
+ penaltyTime: 0,
+ injured: false
+ };
+
+ this.aiState = {
+ target: null,
+ behavior: 'defensive',
+ lastAction: 0,
+ reactionTime: 50 + Math.random() * 100
+ };
+
+ this.homePosition = new Vector2(x, y);
+ this.angle = 0;
+ this.targetAngle = 0;
+ }
+
+ update(deltaTime, gameState, puck, players) {
+ if (this.state.penaltyTime > 0) {
+ this.state.penaltyTime -= deltaTime;
+ return;
+ }
+
+ this.updateEnergy(deltaTime);
+ this.updateMovement(deltaTime);
+ this.updateAngle(deltaTime);
+
+ if (this.role !== 'G') {
+ this.updateAI(gameState, puck, players);
+ } else {
+ this.updateGoalie(gameState, puck, players);
+ }
+ }
+
+ updateEnergy(deltaTime) {
+ const energyDrain = this.velocity.magnitude() / this.maxSpeed * 10 * deltaTime;
+ this.state.energy = Math.max(0, this.state.energy - energyDrain);
+
+ if (this.state.energy < 20) {
+ this.maxSpeed *= 0.7;
+ }
+
+ if (this.velocity.magnitude() < 50) {
+ this.state.energy = Math.min(100, this.state.energy + 15 * deltaTime);
+ }
+ }
+
+ updateMovement(deltaTime) {
+ const direction = this.targetPosition.subtract(this.position).normalize();
+ const distance = this.position.distance(this.targetPosition);
+
+ if (distance > 10) {
+ const force = direction.multiply(this.acceleration * deltaTime);
+ this.velocity = this.velocity.add(force);
+ } else {
+ // Slow down when close to target
+ this.velocity = this.velocity.multiply(0.8);
+ }
+
+ const speedMultiplier = Math.min(1, this.state.energy / 100);
+ this.velocity = this.velocity.limit(this.maxSpeed * speedMultiplier);
+
+ // Reduced friction for more responsive movement
+ this.velocity = Physics.applyFriction(this.velocity, 2, deltaTime);
+
+ this.position = this.position.add(this.velocity.multiply(deltaTime));
+
+ this.keepInBounds();
+ }
+
+ updateAngle(deltaTime) {
+ let angleDiff = this.targetAngle - this.angle;
+ angleDiff = Physics.wrapAngle(angleDiff);
+
+ const turnRate = 10;
+ if (Math.abs(angleDiff) > 0.1) {
+ this.angle += Math.sign(angleDiff) * turnRate * deltaTime;
+ this.angle = Physics.wrapAngle(this.angle);
+ }
+ }
+
+ updateAI(gameState, puck, players) {
+ const currentTime = Date.now();
+ if (currentTime - this.aiState.lastAction < this.aiState.reactionTime) {
+ return;
+ }
+
+ this.aiState.lastAction = currentTime;
+
+ const distanceToPuck = this.position.distance(puck.position);
+ const teammates = players.filter(p => p.team === this.team && p.id !== this.id);
+ const opponents = players.filter(p => p.team !== this.team);
+
+ if (this.state.hasPuck) {
+ this.behaviorWithPuck(gameState, puck, teammates, opponents);
+ } else {
+ this.behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck);
+ }
+ }
+
+ behaviorWithPuck(gameState, puck, teammates, opponents) {
+ const enemyGoal = this.team === 'home' ?
+ new Vector2(gameState.rink.width - 50, gameState.rink.centerY) :
+ new Vector2(50, gameState.rink.centerY);
+
+ const nearestOpponent = this.findNearestPlayer(opponents);
+ const distanceToGoal = this.position.distance(enemyGoal);
+
+ if (distanceToGoal < 200 && Math.random() < 0.3) {
+ this.shoot(puck, enemyGoal);
+ } else if (nearestOpponent && this.position.distance(nearestOpponent.position) < 80) {
+ const bestTeammate = this.findBestPassTarget(teammates, opponents);
+ if (bestTeammate && Math.random() < 0.7) {
+ this.pass(puck, bestTeammate);
+ } else {
+ this.moveToPosition(enemyGoal);
+ }
+ } else {
+ this.moveToPosition(enemyGoal);
+ }
+ }
+
+ behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck) {
+ const puckOwner = opponents.find(p => p.state.hasPuck) || teammates.find(p => p.state.hasPuck);
+
+ if (!puckOwner && distanceToPuck < 200) {
+ this.chasePuck(puck);
+ } else if (puckOwner && puckOwner.team !== this.team) {
+ if (distanceToPuck < 150 && Math.random() < 0.2) {
+ this.checkPlayer(puckOwner);
+ } else {
+ this.defendPosition(gameState, puckOwner);
+ }
+ } else {
+ this.moveToFormationPosition(gameState);
+ }
+ }
+
+ updateGoalie(gameState, puck, players) {
+ const goal = this.team === 'home' ?
+ new Vector2(50, gameState.rink.centerY) :
+ new Vector2(gameState.rink.width - 50, gameState.rink.centerY);
+
+ const crease = {
+ x: goal.x - 30,
+ y: goal.y - 60,
+ width: 60,
+ height: 120
+ };
+
+ if (this.position.distance(puck.position) < 80) {
+ this.targetPosition = puck.position.lerp(goal, 0.3);
+ } else {
+ this.targetPosition = goal.add(new Vector2(
+ this.team === 'home' ? 20 : -20,
+ (puck.position.y - goal.y) * 0.3
+ ));
+ }
+
+ this.targetPosition.x = Math.max(crease.x, Math.min(crease.x + crease.width, this.targetPosition.x));
+ this.targetPosition.y = Math.max(crease.y, Math.min(crease.y + crease.height, this.targetPosition.y));
+ }
+
+ chasePuck(puck) {
+ this.moveToPosition(puck.position);
+ this.aiState.behavior = 'chasing';
+ }
+
+ shoot(puck, target) {
+ const direction = target.subtract(puck.position).normalize();
+ const power = this.attributes.shooting / 100 * 800;
+ const accuracy = this.attributes.shooting / 100;
+
+ const spread = (1 - accuracy) * 0.5;
+ const angle = direction.angle() + (Math.random() - 0.5) * spread;
+
+ puck.velocity = Vector2.fromAngle(angle, power);
+ this.state.hasPuck = false;
+
+ return true;
+ }
+
+ pass(puck, target) {
+ const direction = target.position.subtract(puck.position).normalize();
+ const distance = puck.position.distance(target.position);
+ const power = Math.min(600, distance * 2);
+
+ puck.velocity = direction.multiply(power);
+ this.state.hasPuck = false;
+
+ return true;
+ }
+
+ checkPlayer(target) {
+ if (this.position.distance(target.position) < 30) {
+ target.velocity = target.velocity.add(
+ this.position.subtract(target.position).normalize().multiply(-200)
+ );
+ this.aiState.behavior = 'checking';
+ return true;
+ }
+ this.moveToPosition(target.position);
+ return false;
+ }
+
+ moveToPosition(target) {
+ this.targetPosition = target.copy();
+ this.targetAngle = target.subtract(this.position).angle();
+ }
+
+ defendPosition(gameState, opponent) {
+ const ownGoal = this.team === 'home' ?
+ new Vector2(50, gameState.rink.centerY) :
+ new Vector2(gameState.rink.width - 50, gameState.rink.centerY);
+
+ const defendPoint = opponent.position.lerp(ownGoal, 0.6);
+ this.moveToPosition(defendPoint);
+ this.aiState.behavior = 'defending';
+ }
+
+ moveToFormationPosition(gameState) {
+ this.moveToPosition(this.getFormationPosition(gameState));
+ this.aiState.behavior = 'formation';
+ }
+
+ getFormationPosition(gameState) {
+ const side = this.team === 'home' ? -1 : 1;
+ const centerX = gameState.rink.centerX;
+ const centerY = gameState.rink.centerY;
+
+ switch (this.role) {
+ case 'C':
+ return new Vector2(centerX + side * 100, centerY);
+ case 'LW':
+ return new Vector2(centerX + side * 150, centerY - 100);
+ case 'RW':
+ return new Vector2(centerX + side * 150, centerY + 100);
+ case 'LD':
+ return new Vector2(centerX + side * 200, centerY - 80);
+ case 'RD':
+ return new Vector2(centerX + side * 200, centerY + 80);
+ default:
+ return this.homePosition;
+ }
+ }
+
+ findNearestPlayer(players) {
+ let nearest = null;
+ let minDistance = Infinity;
+
+ players.forEach(player => {
+ const distance = this.position.distance(player.position);
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearest = player;
+ }
+ });
+
+ return nearest;
+ }
+
+ findBestPassTarget(teammates, opponents) {
+ let bestTarget = null;
+ let bestScore = -1;
+
+ teammates.forEach(teammate => {
+ const distance = this.position.distance(teammate.position);
+ if (distance < 50 || distance > 300) return;
+
+ let blocked = false;
+ opponents.forEach(opponent => {
+ const lineToTeammate = teammate.position.subtract(this.position);
+ const lineToOpponent = opponent.position.subtract(this.position);
+ const angle = Math.abs(lineToTeammate.angle() - lineToOpponent.angle());
+
+ if (angle < 0.3 && this.position.distance(opponent.position) < distance) {
+ blocked = true;
+ }
+ });
+
+ if (!blocked) {
+ const score = teammate.attributes.puckHandling / distance;
+ if (score > bestScore) {
+ bestScore = score;
+ bestTarget = teammate;
+ }
+ }
+ });
+
+ return bestTarget;
+ }
+
+ keepInBounds() {
+ 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));
+ }
+
+ render(ctx) {
+ ctx.save();
+ ctx.translate(this.position.x, this.position.y);
+ ctx.rotate(this.angle);
+
+ ctx.fillStyle = this.team === 'home' ? '#4a90e2' : '#e24a4a';
+ ctx.strokeStyle = '#fff';
+ ctx.lineWidth = 2;
+
+ ctx.beginPath();
+ ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+
+ if (this.state.hasPuck) {
+ ctx.fillStyle = '#ffff00';
+ ctx.beginPath();
+ ctx.arc(0, -this.radius - 5, 3, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ ctx.fillStyle = '#fff';
+ ctx.font = '10px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText(this.role, 0, 3);
+
+ ctx.restore();
+ }
+}
\ No newline at end of file
diff --git a/src/entities/puck.js b/src/entities/puck.js
new file mode 100644
index 0000000..81514eb
--- /dev/null
+++ b/src/entities/puck.js
@@ -0,0 +1,272 @@
+class Puck {
+ constructor(x = 500, y = 300) {
+ this.position = new Vector2(x, y);
+ this.velocity = new Vector2(0, 0);
+ this.radius = 8;
+ this.mass = 0.5;
+ this.restitution = 0.9;
+ this.friction = 3;
+
+ this.lastPlayerTouch = null;
+ this.lastTeamTouch = null;
+ this.bounceCount = 0;
+ this.trail = [];
+ this.maxTrailLength = 10;
+ }
+
+ update(deltaTime, gameState, players) {
+ this.updatePosition(deltaTime);
+ this.checkBoardCollisions(gameState);
+ this.checkPlayerCollisions(players, gameState);
+ this.updateTrail();
+ }
+
+ updatePosition(deltaTime) {
+ this.velocity = Physics.applyFriction(this.velocity, this.friction, deltaTime);
+ this.position = this.position.add(this.velocity.multiply(deltaTime));
+ }
+
+ updateTrail() {
+ if (this.velocity.magnitude() > 50) {
+ this.trail.unshift({
+ position: this.position.copy(),
+ alpha: 1.0
+ });
+
+ if (this.trail.length > this.maxTrailLength) {
+ this.trail.pop();
+ }
+
+ this.trail.forEach((point, index) => {
+ point.alpha = 1 - (index / this.maxTrailLength);
+ });
+ }
+ }
+
+ checkBoardCollisions(gameState) {
+ const rink = gameState.rink;
+ let collision = false;
+
+ if (this.position.x - this.radius <= 0 || this.position.x + this.radius >= rink.width) {
+ if (this.isInGoal(gameState)) {
+ this.handleGoal(gameState);
+ return;
+ }
+ this.velocity.x *= -this.restitution;
+ this.position.x = Math.max(this.radius, Math.min(rink.width - this.radius, this.position.x));
+ collision = true;
+ }
+
+ if (this.position.y - this.radius <= 0 || this.position.y + this.radius >= rink.height) {
+ this.velocity.y *= -this.restitution;
+ this.position.y = Math.max(this.radius, Math.min(rink.height - this.radius, this.position.y));
+ collision = true;
+ }
+
+ if (collision) {
+ this.bounceCount++;
+ this.velocity = this.velocity.multiply(0.8);
+ }
+ }
+
+ isInGoal(gameState) {
+ const goalY = gameState.rink.centerY;
+ const goalHeight = gameState.rink.goalHeight;
+
+ if (this.position.y >= goalY - goalHeight && this.position.y <= goalY + goalHeight) {
+ if (this.position.x <= 20 || this.position.x >= gameState.rink.width - 20) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ handleGoal(gameState) {
+ const team = this.position.x <= 20 ? 'away' : 'home';
+ gameState.addGoal(team);
+
+ this.reset(gameState.rink.centerX, gameState.rink.centerY);
+
+ gameState.emit('goal-scored', {
+ team,
+ scorer: this.lastPlayerTouch,
+ position: this.position.copy()
+ });
+ }
+
+ checkPlayerCollisions(players, gameState) {
+ let closestPlayer = null;
+ let closestDistance = Infinity;
+
+ players.forEach(player => {
+ if (player.state.penaltyTime > 0) return;
+
+ const distance = this.position.distance(player.position);
+ const collisionDistance = this.radius + player.radius;
+
+ if (distance < collisionDistance) {
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closestPlayer = player;
+ }
+ }
+ });
+
+ if (closestPlayer) {
+ this.handlePlayerCollision(closestPlayer, gameState);
+ }
+ }
+
+ handlePlayerCollision(player, gameState) {
+ const distance = this.position.distance(player.position);
+ const minDistance = this.radius + player.radius;
+
+ if (distance < minDistance) {
+ const overlap = minDistance - distance;
+ const direction = this.position.subtract(player.position).normalize();
+
+ this.position = player.position.add(direction.multiply(minDistance));
+
+ const relativeVelocity = this.velocity.subtract(player.velocity);
+ const speed = relativeVelocity.dot(direction);
+
+ if (speed > 0) return;
+
+ const totalMass = this.mass + player.mass;
+ const impulse = 2 * speed / totalMass;
+
+ this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution));
+
+ if (this.velocity.magnitude() < 100 && player.role !== 'G') {
+ this.pickupPuck(player, gameState);
+ } else if (player.role === 'G' && this.velocity.magnitude() > 50) {
+ this.handleGoalieSave(player, gameState);
+ }
+ }
+ }
+
+ pickupPuck(player, gameState) {
+ if (player.state.hasPuck) return;
+
+ players.forEach(p => p.state.hasPuck = false);
+
+ player.state.hasPuck = true;
+ this.lastPlayerTouch = player;
+ this.lastTeamTouch = player.team;
+ this.velocity = new Vector2(0, 0);
+
+ gameState.emit('puck-pickup', {
+ player: player.name,
+ team: player.team,
+ position: this.position.copy()
+ });
+ }
+
+ handleGoalieSave(goalie, gameState) {
+ const saveChance = goalie.attributes.defense / 100;
+ const shotSpeed = this.velocity.magnitude();
+ const difficulty = Math.min(1, shotSpeed / 500);
+
+ if (Math.random() < saveChance * (1 - difficulty * 0.5)) {
+ this.velocity = this.velocity.multiply(-0.3);
+ gameState.addSave(goalie.team);
+
+ gameState.emit('save', {
+ goalie: goalie.name,
+ team: goalie.team,
+ difficulty: difficulty
+ });
+ }
+ }
+
+ shoot(direction, power) {
+ this.velocity = direction.normalize().multiply(power);
+ this.bounceCount = 0;
+ this.trail = [];
+ }
+
+ pass(target, power = 300) {
+ const direction = target.subtract(this.position).normalize();
+ this.velocity = direction.multiply(power);
+ }
+
+ reset(x = 500, y = 300) {
+ this.position = new Vector2(x, y);
+ this.velocity = new Vector2(0, 0);
+ this.lastPlayerTouch = null;
+ this.lastTeamTouch = null;
+ this.bounceCount = 0;
+ this.trail = [];
+ }
+
+ faceoff(player1, player2) {
+ const centerPoint = player1.position.add(player2.position).divide(2);
+ this.position = centerPoint.copy();
+ this.velocity = new Vector2(0, 0);
+
+ const winner = Math.random() < 0.5 ? player1 : player2;
+ const loser = winner === player1 ? player2 : player1;
+
+ // Clear any existing puck possession
+ player1.state.hasPuck = false;
+ player2.state.hasPuck = false;
+
+ setTimeout(() => {
+ // Winner gets the puck with slight movement toward their goal
+ const direction = winner.team === 'home' ?
+ new Vector2(1, (Math.random() - 0.5) * 0.5) :
+ new Vector2(-1, (Math.random() - 0.5) * 0.5);
+
+ this.velocity = direction.normalize().multiply(100);
+ winner.state.hasPuck = true;
+ this.lastPlayerTouch = winner;
+ this.lastTeamTouch = winner.team;
+ }, 500);
+
+ return winner;
+ }
+
+ isLoose(players) {
+ return !players.some(player => player.state.hasPuck);
+ }
+
+ getSpeed() {
+ return this.velocity.magnitude();
+ }
+
+ render(ctx) {
+ this.trail.forEach((point, index) => {
+ if (point.alpha > 0) {
+ ctx.save();
+ ctx.globalAlpha = point.alpha * 0.5;
+ ctx.fillStyle = '#ffff88';
+ ctx.beginPath();
+ ctx.arc(point.position.x, point.position.y, this.radius * (point.alpha * 0.5 + 0.5), 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+ }
+ });
+
+ ctx.save();
+ ctx.fillStyle = '#333';
+ ctx.strokeStyle = '#fff';
+ ctx.lineWidth = 2;
+
+ ctx.beginPath();
+ ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+
+ if (this.velocity.magnitude() > 20) {
+ ctx.strokeStyle = '#ffff88';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ const direction = this.velocity.normalize().multiply(-15);
+ ctx.moveTo(this.position.x, this.position.y);
+ ctx.lineTo(this.position.x + direction.x, this.position.y + direction.y);
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ }
+}
\ No newline at end of file
diff --git a/src/systems/ai-system.js b/src/systems/ai-system.js
new file mode 100644
index 0000000..080364c
--- /dev/null
+++ b/src/systems/ai-system.js
@@ -0,0 +1,268 @@
+class AISystem {
+ constructor() {
+ this.formations = {
+ offensive: {
+ 'LW': { x: 0.7, y: 0.2 },
+ 'C': { x: 0.75, y: 0.5 },
+ 'RW': { x: 0.7, y: 0.8 },
+ 'LD': { x: 0.4, y: 0.3 },
+ 'RD': { x: 0.4, y: 0.7 }
+ },
+ defensive: {
+ 'LW': { x: 0.3, y: 0.2 },
+ 'C': { x: 0.4, y: 0.5 },
+ 'RW': { x: 0.3, y: 0.8 },
+ 'LD': { x: 0.25, y: 0.3 },
+ 'RD': { x: 0.25, y: 0.7 }
+ },
+ neutral: {
+ 'LW': { x: 0.5, y: 0.25 },
+ 'C': { x: 0.5, y: 0.5 },
+ 'RW': { x: 0.5, y: 0.75 },
+ 'LD': { x: 0.35, y: 0.35 },
+ 'RD': { x: 0.35, y: 0.65 }
+ }
+ };
+
+ this.strategyWeights = {
+ aggressive: { offense: 0.7, defense: 0.3 },
+ balanced: { offense: 0.5, defense: 0.5 },
+ defensive: { offense: 0.3, defense: 0.7 }
+ };
+
+ this.currentStrategy = 'balanced';
+ }
+
+ update(players, puck, gameState) {
+ const puckOwner = this.findPuckOwner(players);
+ const gameContext = this.analyzeGameContext(players, puck, gameState, puckOwner);
+
+ players.forEach(player => {
+ if (player.role !== 'G') {
+ this.updatePlayerAI(player, gameContext);
+ }
+ });
+ }
+
+ findPuckOwner(players) {
+ return players.find(player => player.state.hasPuck) || null;
+ }
+
+ analyzeGameContext(players, puck, gameState, puckOwner) {
+ const context = {
+ puckOwner,
+ puckPosition: puck.position,
+ gameTime: gameState.timeRemaining,
+ period: gameState.period,
+ scoreGap: gameState.homeScore - gameState.awayScore,
+ powerPlay: gameState.powerPlay,
+ zone: this.determinePuckZone(puck, gameState)
+ };
+
+ context.formation = this.selectFormation(context);
+ context.urgency = this.calculateUrgency(context);
+
+ return context;
+ }
+
+ determinePuckZone(puck, gameState) {
+ const rinkWidth = gameState.rink.width;
+ const x = puck.position.x;
+
+ if (x < rinkWidth * 0.33) return 'defensive';
+ if (x > rinkWidth * 0.67) return 'offensive';
+ return 'neutral';
+ }
+
+ selectFormation(context) {
+ if (context.powerPlay.home || context.powerPlay.away) {
+ return context.powerPlay.home ? 'offensive' : 'defensive';
+ }
+
+ switch (context.zone) {
+ case 'offensive':
+ return 'offensive';
+ case 'defensive':
+ return 'defensive';
+ default:
+ return 'neutral';
+ }
+ }
+
+ calculateUrgency(context) {
+ let urgency = 0;
+
+ if (context.gameTime < 300) urgency += 0.3;
+ if (context.gameTime < 120) urgency += 0.3;
+ if (Math.abs(context.scoreGap) > 1) urgency += 0.2;
+ if (context.powerPlay.home || context.powerPlay.away) urgency += 0.4;
+
+ return Math.min(1, urgency);
+ }
+
+ updatePlayerAI(player, context) {
+ const teamSide = player.team === 'home' ? 1 : -1;
+ const formation = this.formations[context.formation];
+ const playerFormation = formation[player.role];
+
+ if (playerFormation) {
+ const formationPosition = this.calculateFormationPosition(
+ playerFormation,
+ context,
+ teamSide
+ );
+
+ player.aiState.formationTarget = formationPosition;
+ }
+
+ this.updatePlayerBehavior(player, context);
+ }
+
+ calculateFormationPosition(formation, context, teamSide) {
+ const rink = { width: 1000, height: 600 };
+ const centerX = rink.width * 0.5;
+ const centerY = rink.height * 0.5;
+
+ let x, y;
+
+ if (teamSide === 1) {
+ x = formation.x * rink.width;
+ y = formation.y * rink.height;
+ } else {
+ x = (1 - formation.x) * rink.width;
+ y = formation.y * rink.height;
+ }
+
+ const puckInfluence = this.calculatePuckInfluence(context.puckPosition, new Vector2(x, y));
+ x += puckInfluence.x * 30;
+ y += puckInfluence.y * 30;
+
+ return new Vector2(x, y);
+ }
+
+ calculatePuckInfluence(puckPos, playerPos) {
+ const direction = puckPos.subtract(playerPos).normalize();
+ const distance = puckPos.distance(playerPos);
+ const influence = Math.max(0, 1 - distance / 200);
+
+ return direction.multiply(influence);
+ }
+
+ updatePlayerBehavior(player, context) {
+ const distanceToPuck = player.position.distance(context.puckPosition);
+ const isNearPuck = distanceToPuck < 100;
+
+ if (player.state.hasPuck) {
+ player.aiState.behavior = 'puck_carrier';
+ this.executePuckCarrierBehavior(player, context);
+ } else if (context.puckOwner && context.puckOwner.team === player.team) {
+ player.aiState.behavior = 'support';
+ this.executeSupportBehavior(player, context);
+ } else if (context.puckOwner && context.puckOwner.team !== player.team) {
+ player.aiState.behavior = 'pressure';
+ this.executePressureBehavior(player, context);
+ } else if (isNearPuck) {
+ player.aiState.behavior = 'chase';
+ this.executeChaseBehavior(player, context);
+ } else {
+ player.aiState.behavior = 'formation';
+ this.executeFormationBehavior(player, context);
+ }
+ }
+
+ executePuckCarrierBehavior(player, context) {
+ const enemyGoal = player.team === 'home' ?
+ new Vector2(950, 300) : new Vector2(50, 300);
+
+ const distanceToGoal = player.position.distance(enemyGoal);
+ const urgency = context.urgency;
+
+ if (distanceToGoal < 200 && urgency > 0.5) {
+ player.aiState.action = 'shoot';
+ } else if (this.isUnderPressure(player, context)) {
+ player.aiState.action = 'pass';
+ } else {
+ player.aiState.action = 'advance';
+ player.targetPosition = enemyGoal.copy();
+ }
+ }
+
+ executeSupportBehavior(player, context) {
+ const puckCarrier = context.puckOwner;
+ const supportPosition = this.calculateSupportPosition(player, puckCarrier, context);
+
+ player.targetPosition = supportPosition;
+ player.aiState.action = 'support';
+ }
+
+ executePressureBehavior(player, context) {
+ const opponent = context.puckOwner;
+ const pressurePosition = this.calculatePressurePosition(player, opponent, context);
+
+ player.targetPosition = pressurePosition;
+ player.aiState.action = 'pressure';
+ }
+
+ executeChaseBehavior(player, context) {
+ player.targetPosition = context.puckPosition.copy();
+ player.aiState.action = 'chase';
+ }
+
+ executeFormationBehavior(player, context) {
+ if (player.aiState.formationTarget) {
+ player.targetPosition = player.aiState.formationTarget.copy();
+ }
+ player.aiState.action = 'formation';
+ }
+
+ isUnderPressure(player, context) {
+ // Check if opponent players are nearby and approaching
+ const nearbyOpponents = this.getNearbyOpponents(player, context, 80);
+ return nearbyOpponents.length > 0;
+ }
+
+ getNearbyOpponents(player, context, radius) {
+ // This would typically be passed from the game engine
+ // For now, return empty array as placeholder
+ return [];
+ }
+
+ calculateSupportPosition(player, puckCarrier, context) {
+ const enemyGoal = player.team === 'home' ?
+ new Vector2(950, 300) : new Vector2(50, 300);
+
+ const basePosition = puckCarrier.position.lerp(enemyGoal, 0.7);
+ const offset = this.getRoleOffset(player.role);
+
+ return basePosition.add(offset);
+ }
+
+ calculatePressurePosition(player, opponent, context) {
+ const ownGoal = player.team === 'home' ?
+ new Vector2(50, 300) : new Vector2(950, 300);
+
+ return opponent.position.lerp(ownGoal, 0.3);
+ }
+
+ getRoleOffset(role) {
+ const offsets = {
+ 'LW': new Vector2(-50, -80),
+ 'RW': new Vector2(-50, 80),
+ 'C': new Vector2(0, 0),
+ 'LD': new Vector2(-100, -40),
+ 'RD': new Vector2(-100, 40)
+ };
+
+ return offsets[role] || new Vector2(0, 0);
+ }
+
+ setStrategy(strategy) {
+ if (this.strategyWeights[strategy]) {
+ this.currentStrategy = strategy;
+ }
+ }
+
+ getStrategy() {
+ return this.currentStrategy;
+ }
+}
\ No newline at end of file
diff --git a/src/systems/audio-system.js b/src/systems/audio-system.js
new file mode 100644
index 0000000..7d0382a
--- /dev/null
+++ b/src/systems/audio-system.js
@@ -0,0 +1,245 @@
+class AudioSystem {
+ constructor() {
+ this.audioContext = null;
+ this.sounds = {};
+ this.volume = 0.7;
+ this.enabled = true;
+ this.initialized = false;
+
+ this.initializeAudio();
+ }
+
+ async initializeAudio() {
+ try {
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ this.generateSounds();
+ this.initialized = true;
+ } catch (error) {
+ console.warn('Audio initialization failed:', error);
+ this.enabled = false;
+ }
+ }
+
+ generateSounds() {
+ this.sounds = {
+ puckHit: this.createPuckHitSound(),
+ stick: this.createStickSound(),
+ goal: this.createGoalSound(),
+ save: this.createSaveSound(),
+ whistle: this.createWhistleSound(),
+ crowd: this.createCrowdSound(),
+ skate: this.createSkateSound(),
+ board: this.createBoardSound()
+ };
+ }
+
+ createPuckHitSound() {
+ return this.createPercussiveSound(150, 0.1, 'sawtooth');
+ }
+
+ createStickSound() {
+ return this.createPercussiveSound(200, 0.05, 'square');
+ }
+
+ createGoalSound() {
+ return this.createMelodicSound([400, 500, 600], 0.8, 'sine');
+ }
+
+ createSaveSound() {
+ return this.createPercussiveSound(100, 0.3, 'triangle');
+ }
+
+ createWhistleSound() {
+ return this.createMelodicSound([800, 900], 0.5, 'sine');
+ }
+
+ createCrowdSound() {
+ return this.createNoiseSound(0.2, 2.0);
+ }
+
+ createSkateSound() {
+ return this.createNoiseSound(0.1, 0.2);
+ }
+
+ createBoardSound() {
+ return this.createPercussiveSound(80, 0.2, 'triangle');
+ }
+
+ createPercussiveSound(frequency, duration, waveType = 'sine') {
+ return () => {
+ if (!this.enabled || !this.audioContext) return;
+
+ const oscillator = this.audioContext.createOscillator();
+ const gainNode = this.audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(this.audioContext.destination);
+
+ oscillator.type = waveType;
+ oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime);
+ oscillator.frequency.exponentialRampToValueAtTime(
+ frequency * 0.5,
+ this.audioContext.currentTime + duration
+ );
+
+ gainNode.gain.setValueAtTime(this.volume * 0.3, this.audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(
+ 0.001,
+ this.audioContext.currentTime + duration
+ );
+
+ oscillator.start(this.audioContext.currentTime);
+ oscillator.stop(this.audioContext.currentTime + duration);
+ };
+ }
+
+ createMelodicSound(frequencies, duration, waveType = 'sine') {
+ return () => {
+ if (!this.enabled || !this.audioContext) return;
+
+ frequencies.forEach((freq, index) => {
+ const delay = index * 0.1;
+ const oscillator = this.audioContext.createOscillator();
+ const gainNode = this.audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(this.audioContext.destination);
+
+ oscillator.type = waveType;
+ oscillator.frequency.setValueAtTime(freq, this.audioContext.currentTime + delay);
+
+ gainNode.gain.setValueAtTime(0, this.audioContext.currentTime + delay);
+ gainNode.gain.linearRampToValueAtTime(
+ this.volume * 0.2,
+ this.audioContext.currentTime + delay + 0.05
+ );
+ gainNode.gain.exponentialRampToValueAtTime(
+ 0.001,
+ this.audioContext.currentTime + delay + duration
+ );
+
+ oscillator.start(this.audioContext.currentTime + delay);
+ oscillator.stop(this.audioContext.currentTime + delay + duration);
+ });
+ };
+ }
+
+ createNoiseSound(volume, duration) {
+ return () => {
+ if (!this.enabled || !this.audioContext) return;
+
+ const bufferSize = this.audioContext.sampleRate * duration;
+ const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
+ const data = buffer.getChannelData(0);
+
+ for (let i = 0; i < bufferSize; i++) {
+ data[i] = (Math.random() * 2 - 1) * volume * this.volume;
+ }
+
+ const source = this.audioContext.createBufferSource();
+ const gainNode = this.audioContext.createGain();
+ const filter = this.audioContext.createBiquadFilter();
+
+ source.buffer = buffer;
+ source.connect(filter);
+ filter.connect(gainNode);
+ gainNode.connect(this.audioContext.destination);
+
+ filter.type = 'lowpass';
+ filter.frequency.setValueAtTime(2000, this.audioContext.currentTime);
+
+ gainNode.gain.setValueAtTime(1, this.audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + duration);
+
+ source.start(this.audioContext.currentTime);
+ };
+ }
+
+ playSound(soundName, volume = 1) {
+ if (!this.enabled || !this.sounds[soundName]) return;
+
+ try {
+ if (this.audioContext.state === 'suspended') {
+ this.audioContext.resume();
+ }
+ this.sounds[soundName]();
+ } catch (error) {
+ console.warn('Failed to play sound:', soundName, error);
+ }
+ }
+
+ onPuckHit(velocity) {
+ const intensity = Math.min(1, velocity / 500);
+ if (intensity > 0.1) {
+ this.playSound('puckHit', intensity);
+ }
+ }
+
+ onStickContact() {
+ this.playSound('stick');
+ }
+
+ onGoal(team) {
+ this.playSound('goal');
+ setTimeout(() => this.playSound('crowd'), 500);
+ }
+
+ onSave() {
+ this.playSound('save');
+ }
+
+ onPenalty() {
+ this.playSound('whistle');
+ }
+
+ onPeriodEnd() {
+ this.playSound('whistle');
+ }
+
+ onBoardCollision(velocity) {
+ const intensity = Math.min(1, velocity / 300);
+ if (intensity > 0.2) {
+ this.playSound('board', intensity);
+ }
+ }
+
+ onPlayerMovement(speed) {
+ if (speed > 100 && Math.random() < 0.01) {
+ this.playSound('skate', 0.3);
+ }
+ }
+
+ setVolume(volume) {
+ this.volume = Math.max(0, Math.min(1, volume));
+ }
+
+ setEnabled(enabled) {
+ this.enabled = enabled;
+ }
+
+ toggle() {
+ this.enabled = !this.enabled;
+ return this.enabled;
+ }
+
+ resume() {
+ if (this.audioContext && this.audioContext.state === 'suspended') {
+ return this.audioContext.resume();
+ }
+ }
+
+ suspend() {
+ if (this.audioContext && this.audioContext.state === 'running') {
+ return this.audioContext.suspend();
+ }
+ }
+
+ getState() {
+ return {
+ enabled: this.enabled,
+ volume: this.volume,
+ initialized: this.initialized,
+ contextState: this.audioContext ? this.audioContext.state : 'not-initialized'
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/systems/physics-system.js b/src/systems/physics-system.js
new file mode 100644
index 0000000..08290ef
--- /dev/null
+++ b/src/systems/physics-system.js
@@ -0,0 +1,64 @@
+class PhysicsSystem {
+ constructor() {
+ this.gravity = new Vector2(0, 0);
+ this.airResistance = 0.99;
+ this.collisionIterations = 3;
+ }
+
+ update(entities, deltaTime) {
+ entities.forEach(entity => {
+ this.updateEntity(entity, deltaTime);
+ });
+
+ for (let i = 0; i < this.collisionIterations; i++) {
+ this.resolveCollisions(entities);
+ }
+ }
+
+ updateEntity(entity, deltaTime) {
+ if (!entity.velocity || !entity.position) return;
+
+ entity.velocity = entity.velocity.add(this.gravity.multiply(deltaTime));
+ entity.velocity = entity.velocity.multiply(this.airResistance);
+ entity.position = entity.position.add(entity.velocity.multiply(deltaTime));
+ }
+
+ resolveCollisions(entities) {
+ for (let i = 0; i < entities.length; i++) {
+ for (let j = i + 1; j < entities.length; j++) {
+ const entityA = entities[i];
+ const entityB = entities[j];
+
+ if (this.checkCollision(entityA, entityB)) {
+ this.resolveCollision(entityA, entityB);
+ }
+ }
+ }
+ }
+
+ checkCollision(entityA, entityB) {
+ if (!entityA.radius || !entityB.radius) return false;
+
+ const distance = entityA.position.distance(entityB.position);
+ return distance < (entityA.radius + entityB.radius);
+ }
+
+ resolveCollision(entityA, entityB) {
+ Physics.resolveCircleCollision(entityA, entityB);
+ }
+
+ applyForce(entity, force) {
+ if (!entity.velocity) return;
+
+ const acceleration = force.divide(entity.mass || 1);
+ entity.velocity = entity.velocity.add(acceleration);
+ }
+
+ setGravity(gravity) {
+ this.gravity = gravity;
+ }
+
+ setAirResistance(resistance) {
+ this.airResistance = Math.max(0, Math.min(1, resistance));
+ }
+}
\ No newline at end of file
diff --git a/src/systems/renderer.js b/src/systems/renderer.js
new file mode 100644
index 0000000..0c363f1
--- /dev/null
+++ b/src/systems/renderer.js
@@ -0,0 +1,324 @@
+class Renderer {
+ constructor(canvas) {
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d');
+ this.camera = {
+ x: 0,
+ y: 0,
+ zoom: 1,
+ target: null,
+ smoothing: 0.1
+ };
+
+ this.setupCanvas();
+ }
+
+ setupCanvas() {
+ this.canvas.style.imageRendering = 'pixelated';
+ this.ctx.imageSmoothingEnabled = false;
+ }
+
+ clear() {
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ drawRink(gameState) {
+ const rink = gameState.rink;
+
+ this.ctx.save();
+ this.applyCamera();
+
+ this.ctx.fillStyle = '#f8f8f8';
+ this.ctx.fillRect(0, 0, rink.width, rink.height);
+
+ this.drawRinkLines(rink);
+ this.drawGoals(rink);
+ this.drawFaceoffDots(rink);
+ this.drawCreases(rink);
+
+ this.ctx.restore();
+ }
+
+ drawRinkLines(rink) {
+ this.ctx.strokeStyle = '#d32f2f';
+ this.ctx.lineWidth = 3;
+
+ this.ctx.beginPath();
+ this.ctx.rect(0, 0, rink.width, rink.height);
+ this.ctx.stroke();
+
+ this.ctx.strokeStyle = '#2196f3';
+ this.ctx.lineWidth = 2;
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(rink.centerX, 0);
+ this.ctx.lineTo(rink.centerX, rink.height);
+ this.ctx.stroke();
+
+ const zoneWidth = rink.width / 3;
+ this.ctx.strokeStyle = '#2196f3';
+ this.ctx.setLineDash([10, 5]);
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(zoneWidth, 0);
+ this.ctx.lineTo(zoneWidth, rink.height);
+ this.ctx.stroke();
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(rink.width - zoneWidth, 0);
+ this.ctx.lineTo(rink.width - zoneWidth, rink.height);
+ this.ctx.stroke();
+
+ this.ctx.setLineDash([]);
+
+ this.ctx.beginPath();
+ this.ctx.arc(rink.centerX, rink.centerY, 100, 0, Math.PI * 2);
+ this.ctx.stroke();
+ }
+
+ drawGoals(rink) {
+ this.ctx.strokeStyle = '#d32f2f';
+ this.ctx.lineWidth = 4;
+
+ const goalY = rink.centerY;
+ const goalHeight = rink.goalHeight;
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(0, goalY - goalHeight);
+ this.ctx.lineTo(-10, goalY - goalHeight);
+ this.ctx.lineTo(-10, goalY + goalHeight);
+ this.ctx.lineTo(0, goalY + goalHeight);
+ this.ctx.stroke();
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(rink.width, goalY - goalHeight);
+ this.ctx.lineTo(rink.width + 10, goalY - goalHeight);
+ this.ctx.lineTo(rink.width + 10, goalY + goalHeight);
+ this.ctx.lineTo(rink.width, goalY + goalHeight);
+ this.ctx.stroke();
+
+ this.ctx.fillStyle = 'rgba(211, 47, 47, 0.1)';
+ this.ctx.fillRect(-10, goalY - goalHeight, 10, goalHeight * 2);
+ this.ctx.fillRect(rink.width, goalY - goalHeight, 10, goalHeight * 2);
+ }
+
+ drawCreases(rink) {
+ this.ctx.strokeStyle = '#4fc3f7';
+ this.ctx.lineWidth = 2;
+ this.ctx.fillStyle = 'rgba(79, 195, 247, 0.1)';
+
+ const creaseRadius = 60;
+ const goalY = rink.centerY;
+
+ this.ctx.beginPath();
+ this.ctx.arc(60, goalY, creaseRadius, -Math.PI/2, Math.PI/2);
+ this.ctx.fill();
+ this.ctx.stroke();
+
+ this.ctx.beginPath();
+ this.ctx.arc(rink.width - 60, goalY, creaseRadius, Math.PI/2, -Math.PI/2);
+ this.ctx.fill();
+ this.ctx.stroke();
+ }
+
+ drawFaceoffDots(rink) {
+ this.ctx.fillStyle = '#d32f2f';
+
+ rink.faceoffDots.forEach(dot => {
+ this.ctx.beginPath();
+ this.ctx.arc(dot.x, dot.y, 8, 0, Math.PI * 2);
+ this.ctx.fill();
+
+ this.ctx.strokeStyle = '#d32f2f';
+ this.ctx.lineWidth = 2;
+ this.ctx.beginPath();
+ this.ctx.arc(dot.x, dot.y, 30, 0, Math.PI * 2);
+ this.ctx.stroke();
+ });
+ }
+
+ drawPlayers(players) {
+ this.ctx.save();
+ this.applyCamera();
+
+ players.forEach(player => {
+ if (player.state.penaltyTime <= 0) {
+ player.render(this.ctx);
+ }
+ });
+
+ this.ctx.restore();
+ }
+
+ drawPuck(puck) {
+ this.ctx.save();
+ this.applyCamera();
+ puck.render(this.ctx);
+ this.ctx.restore();
+ }
+
+ drawUI(gameState) {
+ this.updateScoreBoard(gameState);
+ this.updateGameStats(gameState);
+ this.updatePenalties(gameState);
+ }
+
+ updateScoreBoard(gameState) {
+ document.querySelector('.team.home .score').textContent = gameState.homeScore;
+ document.querySelector('.team.away .score').textContent = gameState.awayScore;
+ document.getElementById('period').textContent = gameState.getPeriodName();
+ document.getElementById('clock').textContent = gameState.formatTime(gameState.timeRemaining);
+ }
+
+ updateGameStats(gameState) {
+ document.getElementById('home-shots').textContent = gameState.stats.home.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') {
+ this.ctx.save();
+ this.applyCamera();
+
+ switch (type) {
+ case 'goal':
+ this.drawGoalEffect(position);
+ break;
+ case 'hit':
+ this.drawHitEffect(position, color);
+ break;
+ case 'save':
+ this.drawSaveEffect(position);
+ break;
+ }
+
+ this.ctx.restore();
+ }
+
+ drawGoalEffect(position) {
+ this.ctx.fillStyle = '#ffff00';
+ this.ctx.strokeStyle = '#ff8800';
+ this.ctx.lineWidth = 3;
+
+ for (let i = 0; i < 8; i++) {
+ const angle = (i / 8) * Math.PI * 2;
+ const x = position.x + Math.cos(angle) * 30;
+ const y = position.y + Math.sin(angle) * 30;
+
+ this.ctx.beginPath();
+ this.ctx.arc(x, y, 5, 0, Math.PI * 2);
+ this.ctx.fill();
+ this.ctx.stroke();
+ }
+ }
+
+ drawHitEffect(position, color) {
+ this.ctx.strokeStyle = color;
+ this.ctx.lineWidth = 4;
+
+ for (let i = 0; i < 6; i++) {
+ const angle = (i / 6) * Math.PI * 2;
+ const startX = position.x + Math.cos(angle) * 10;
+ const startY = position.y + Math.sin(angle) * 10;
+ const endX = position.x + Math.cos(angle) * 25;
+ const endY = position.y + Math.sin(angle) * 25;
+
+ this.ctx.beginPath();
+ this.ctx.moveTo(startX, startY);
+ this.ctx.lineTo(endX, endY);
+ this.ctx.stroke();
+ }
+ }
+
+ drawSaveEffect(position) {
+ this.ctx.fillStyle = '#4fc3f7';
+ this.ctx.strokeStyle = '#0288d1';
+ this.ctx.lineWidth = 2;
+
+ this.ctx.beginPath();
+ this.ctx.arc(position.x, position.y, 20, 0, Math.PI * 2);
+ this.ctx.stroke();
+
+ this.ctx.beginPath();
+ this.ctx.arc(position.x, position.y, 15, 0, Math.PI * 2);
+ this.ctx.stroke();
+ }
+
+ updateCamera(target) {
+ if (target) {
+ 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() {
+ this.ctx.translate(this.camera.x, this.camera.y);
+ this.ctx.scale(this.camera.zoom, this.camera.zoom);
+ }
+
+ setZoom(zoom) {
+ this.camera.zoom = Math.max(0.5, Math.min(2.0, zoom));
+ }
+
+ screenToWorld(screenPos) {
+ return new Vector2(
+ (screenPos.x - this.camera.x) / this.camera.zoom,
+ (screenPos.y - this.camera.y) / this.camera.zoom
+ );
+ }
+
+ worldToScreen(worldPos) {
+ return new Vector2(
+ worldPos.x * this.camera.zoom + this.camera.x,
+ worldPos.y * this.camera.zoom + this.camera.y
+ );
+ }
+
+ drawDebugInfo(gameState, players, puck) {
+ if (!window.debugMode) return;
+
+ this.ctx.save();
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+ this.ctx.fillRect(10, 10, 200, 150);
+
+ this.ctx.fillStyle = '#fff';
+ this.ctx.font = '12px monospace';
+ this.ctx.fillText(`FPS: ${Math.round(1000 / 16)}`, 20, 30);
+ this.ctx.fillText(`Players: ${players.length}`, 20, 50);
+ this.ctx.fillText(`Puck Speed: ${Math.round(puck.velocity.magnitude())}`, 20, 70);
+ this.ctx.fillText(`Game Time: ${gameState.formatTime(gameState.timeRemaining)}`, 20, 90);
+ this.ctx.fillText(`Period: ${gameState.period}`, 20, 110);
+ this.ctx.fillText(`Paused: ${gameState.isPaused}`, 20, 130);
+ this.ctx.fillText(`Speed: ${gameState.gameSpeed}x`, 20, 150);
+
+ this.ctx.restore();
+ }
+}
\ No newline at end of file
diff --git a/src/systems/rules-system.js b/src/systems/rules-system.js
new file mode 100644
index 0000000..11da552
--- /dev/null
+++ b/src/systems/rules-system.js
@@ -0,0 +1,256 @@
+class RulesSystem {
+ constructor(gameState) {
+ this.gameState = gameState;
+ this.lastOffsideCheck = 0;
+ this.lastIcingCheck = 0;
+ this.penaltyQueue = [];
+ }
+
+ update(players, puck, deltaTime) {
+ this.checkOffside(players, puck);
+ this.checkIcing(players, puck);
+ this.checkGoaltenderInterference(players, puck);
+ this.checkHighSticking(players, puck);
+ this.processPenaltyQueue();
+ }
+
+ checkOffside(players, puck) {
+ const currentTime = Date.now();
+ if (currentTime - this.lastOffsideCheck < 500) return;
+
+ const puckOwner = players.find(p => p.state.hasPuck);
+ if (!puckOwner) return;
+
+ const centerLine = this.gameState.rink.centerX;
+ const offensiveZoneLine = puckOwner.team === 'home' ?
+ this.gameState.rink.width * 0.67 : this.gameState.rink.width * 0.33;
+
+ const puckInOffensiveZone = puckOwner.team === 'home' ?
+ puck.position.x > offensiveZoneLine :
+ puck.position.x < offensiveZoneLine;
+
+ if (puckInOffensiveZone) {
+ const teammates = players.filter(p =>
+ p.team === puckOwner.team &&
+ p.id !== puckOwner.id &&
+ p.role !== 'G'
+ );
+
+ const offsidePlayers = teammates.filter(teammate => {
+ const isAhead = puckOwner.team === 'home' ?
+ teammate.position.x > puck.position.x :
+ teammate.position.x < puck.position.x;
+
+ const inOffensiveZone = puckOwner.team === 'home' ?
+ teammate.position.x > offensiveZoneLine :
+ teammate.position.x < offensiveZoneLine;
+
+ return isAhead && inOffensiveZone;
+ });
+
+ if (offsidePlayers.length > 0) {
+ this.callOffside(puckOwner.team, offsidePlayers);
+ this.lastOffsideCheck = currentTime;
+ }
+ }
+ }
+
+ callOffside(team, players) {
+ this.gameState.addEvent(`OFFSIDE - ${team.toUpperCase()}`);
+
+ const faceoffPosition = this.gameState.rink.faceoffDots[
+ team === 'home' ? 1 : 3
+ ];
+
+ this.gameState.emit('offside', {
+ team,
+ players: players.map(p => p.name),
+ faceoffPosition
+ });
+
+ this.scheduleFaceoff(faceoffPosition);
+ }
+
+ checkIcing(players, puck) {
+ const currentTime = Date.now();
+ if (currentTime - this.lastIcingCheck < 1000) return;
+
+ if (!puck.lastTeamTouch) return;
+
+ const shootingTeam = puck.lastTeamTouch;
+ const shootingZoneLine = shootingTeam === 'home' ?
+ this.gameState.rink.width * 0.33 : this.gameState.rink.width * 0.67;
+
+ const puckCrossedOwnZone = shootingTeam === 'home' ?
+ puck.position.x < shootingZoneLine :
+ puck.position.x > shootingZoneLine;
+
+ if (!puckCrossedOwnZone) return;
+
+ const goalLine = shootingTeam === 'home' ?
+ this.gameState.rink.width - 50 : 50;
+
+ const puckPastGoalLine = shootingTeam === 'home' ?
+ puck.position.x > goalLine :
+ puck.position.x < goalLine;
+
+ if (puckPastGoalLine) {
+ const shootingPlayers = players.filter(p => p.team === shootingTeam);
+ const opposingPlayers = players.filter(p => p.team !== shootingTeam);
+
+ const nearestShootingPlayer = this.findNearestPlayer(puck.position, shootingPlayers);
+ const nearestOpposingPlayer = this.findNearestPlayer(puck.position, opposingPlayers);
+
+ if (nearestOpposingPlayer &&
+ nearestOpposingPlayer.position.distance(puck.position) <
+ nearestShootingPlayer.position.distance(puck.position)) {
+
+ this.callIcing(shootingTeam);
+ this.lastIcingCheck = currentTime;
+ }
+ }
+ }
+
+ callIcing(team) {
+ this.gameState.addEvent(`ICING - ${team.toUpperCase()}`);
+
+ const faceoffPosition = this.gameState.rink.faceoffDots[
+ team === 'home' ? 0 : 4
+ ];
+
+ this.gameState.emit('icing', {
+ team,
+ 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) {
+ setTimeout(() => {
+ this.gameState.emit('faceoff', { position });
+ }, 2000);
+ }
+
+ findNearestPlayer(position, players) {
+ let nearest = null;
+ let minDistance = Infinity;
+
+ players.forEach(player => {
+ const distance = player.position.distance(position);
+ if (distance < minDistance) {
+ minDistance = distance;
+ nearest = player;
+ }
+ });
+
+ 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() {
+ this.lastOffsideCheck = 0;
+ this.lastIcingCheck = 0;
+ this.penaltyQueue = [];
+ }
+}
\ No newline at end of file
diff --git a/src/utils/physics.js b/src/utils/physics.js
new file mode 100644
index 0000000..85cc65c
--- /dev/null
+++ b/src/utils/physics.js
@@ -0,0 +1,69 @@
+class Physics {
+ static checkCircleCollision(pos1, radius1, pos2, radius2) {
+ const distance = pos1.distance(pos2);
+ return distance < (radius1 + radius2);
+ }
+
+ static resolveCircleCollision(obj1, obj2) {
+ const distance = obj1.position.distance(obj2.position);
+ const minDistance = obj1.radius + obj2.radius;
+
+ if (distance < minDistance) {
+ const overlap = minDistance - distance;
+ const direction = obj2.position.subtract(obj1.position).normalize();
+
+ const separation = direction.multiply(overlap * 0.5);
+ obj1.position = obj1.position.subtract(separation);
+ obj2.position = obj2.position.add(separation);
+
+ const relativeVelocity = obj2.velocity.subtract(obj1.velocity);
+ const speed = relativeVelocity.dot(direction);
+
+ if (speed > 0) return;
+
+ const restitution = Math.min(obj1.restitution || 0.8, obj2.restitution || 0.8);
+ const impulse = 2 * speed / (obj1.mass + obj2.mass);
+
+ obj1.velocity = obj1.velocity.add(direction.multiply(impulse * obj2.mass * restitution));
+ obj2.velocity = obj2.velocity.subtract(direction.multiply(impulse * obj1.mass * restitution));
+ }
+ }
+
+ static checkPointInRectangle(point, rect) {
+ return point.x >= rect.x &&
+ point.x <= rect.x + rect.width &&
+ point.y >= rect.y &&
+ point.y <= rect.y + rect.height;
+ }
+
+ static checkCircleRectangleCollision(circle, rect) {
+ const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
+ const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));
+
+ const distance = new Vector2(circle.x - closestX, circle.y - closestY).magnitude();
+ return distance < circle.radius;
+ }
+
+ static applyFriction(velocity, friction, deltaTime) {
+ const speed = velocity.magnitude();
+ if (speed > 0) {
+ const frictionForce = speed * friction * deltaTime;
+ if (frictionForce >= speed) {
+ return new Vector2(0, 0);
+ } else {
+ return velocity.subtract(velocity.normalize().multiply(frictionForce));
+ }
+ }
+ return velocity;
+ }
+
+ static wrapAngle(angle) {
+ while (angle > Math.PI) angle -= 2 * Math.PI;
+ while (angle < -Math.PI) angle += 2 * Math.PI;
+ return angle;
+ }
+
+ static lerp(start, end, t) {
+ return start + (end - start) * t;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/vector.js b/src/utils/vector.js
new file mode 100644
index 0000000..66000c7
--- /dev/null
+++ b/src/utils/vector.js
@@ -0,0 +1,76 @@
+class Vector2 {
+ constructor(x = 0, y = 0) {
+ this.x = x;
+ this.y = y;
+ }
+
+ static from(obj) {
+ return new Vector2(obj.x, obj.y);
+ }
+
+ copy() {
+ return new Vector2(this.x, this.y);
+ }
+
+ add(vector) {
+ return new Vector2(this.x + vector.x, this.y + vector.y);
+ }
+
+ subtract(vector) {
+ return new Vector2(this.x - vector.x, this.y - vector.y);
+ }
+
+ multiply(scalar) {
+ return new Vector2(this.x * scalar, this.y * scalar);
+ }
+
+ divide(scalar) {
+ return new Vector2(this.x / scalar, this.y / scalar);
+ }
+
+ magnitude() {
+ return Math.sqrt(this.x * this.x + this.y * this.y);
+ }
+
+ normalize() {
+ const mag = this.magnitude();
+ if (mag === 0) return new Vector2(0, 0);
+ return this.divide(mag);
+ }
+
+ distance(vector) {
+ const dx = this.x - vector.x;
+ const dy = this.y - vector.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ dot(vector) {
+ return this.x * vector.x + this.y * vector.y;
+ }
+
+ angle() {
+ return Math.atan2(this.y, this.x);
+ }
+
+ static fromAngle(angle, magnitude = 1) {
+ return new Vector2(
+ Math.cos(angle) * magnitude,
+ Math.sin(angle) * magnitude
+ );
+ }
+
+ lerp(target, t) {
+ return new Vector2(
+ this.x + (target.x - this.x) * t,
+ this.y + (target.y - this.y) * t
+ );
+ }
+
+ limit(maxMagnitude) {
+ const mag = this.magnitude();
+ if (mag > maxMagnitude) {
+ return this.normalize().multiply(maxMagnitude);
+ }
+ return this.copy();
+ }
+}
\ No newline at end of file