diff --git a/src/engine/game-engine.js b/src/engine/game-engine.js index 5e50185..50cafc4 100644 --- a/src/engine/game-engine.js +++ b/src/engine/game-engine.js @@ -7,6 +7,7 @@ class GameEngine { this.players = []; this.puck = new Puck(500, 300); + this.puckActive = false; // Puck starts inactive for faceoff this.lastTime = 0; this.deltaTime = 0; @@ -104,6 +105,18 @@ class GameEngine { this.gameState.on('period-end', () => { this.audioSystem.onPeriodEnd(); }); + + this.gameState.on('faceoff-start', (data) => { + this.puckActive = false; + }); + + this.gameState.on('faceoff-drop', () => { + this.puckActive = true; + }); + + this.gameState.on('faceoff-complete', (data) => { + this.puck.faceoffDrop(data.winner, data.location, this.gameState.faceoff.participants); + }); } setupControls() { @@ -178,17 +191,19 @@ class GameEngine { if (this.gameState.isPaused) return; this.gameState.updateTime(deltaTime); + this.gameState.updateFaceoff(deltaTime); this.players.forEach(player => { player.update(deltaTime, this.gameState, this.puck, this.players); }); - this.puck.update(deltaTime, this.gameState, this.players); + if (this.puckActive) { + this.puck.update(deltaTime, this.gameState, this.players); + this.updateCollisions(); + this.renderer.updateCamera(this.puck.position); + } - this.updateCollisions(); this.updateEffects(deltaTime); - - this.renderer.updateCamera(this.puck.position); } updateCollisions() { @@ -240,7 +255,11 @@ class GameEngine { this.renderer.clear(); this.renderer.drawRink(this.gameState); this.renderer.drawPlayers(this.players); - this.renderer.drawPuck(this.puck); + + if (this.puckActive) { + this.renderer.drawPuck(this.puck); + } + this.renderEffects(); this.renderer.drawUI(this.gameState); this.renderer.drawDebugInfo(this.gameState, this.players, this.puck); @@ -268,42 +287,25 @@ class GameEngine { this.effects.push(effect); } - startFaceoff(position = null) { - if (!position) { - position = new Vector2(this.gameState.rink.centerX, this.gameState.rink.centerY); + startFaceoff(location = null) { + if (!location) { + location = { x: this.gameState.rink.centerX, y: this.gameState.rink.centerY }; } - this.puck.reset(position.x, position.y); + console.log('GameEngine.startFaceoff called with location:', location); - const nearbyPlayers = this.players.filter(player => - player.position.distance(position) < 100 - ).sort((a, b) => - a.position.distance(position) - b.position.distance(position) - ); + // Reset puck position but keep it inactive + this.puck.reset(location.x, location.y); + this.puckActive = false; - 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; - } - } + // Start the faceoff sequence + this.gameState.startFaceoff(location); } resetGame() { this.gameState.reset(); this.effects = []; + this.puckActive = false; // Clear all player states first this.players.forEach(player => { diff --git a/src/engine/game-state.js b/src/engine/game-state.js index 3f20575..86aebf0 100644 --- a/src/engine/game-state.js +++ b/src/engine/game-state.js @@ -56,6 +56,14 @@ class GameState { home: null, away: null }; + + this.faceoff = { + isActive: false, + phase: 'none', // 'setup', 'ready', 'drop', 'complete' + timeRemaining: 0, + location: { x: 500, y: 300 }, // Center ice by default + participants: { home: null, away: null } + }; } formatTime(seconds) { @@ -192,6 +200,67 @@ class GameState { } } + startFaceoff(location = { x: 500, y: 300 }) { + console.log('Starting faceoff at location:', location); + this.faceoff = { + isActive: true, + phase: 'setup', + timeRemaining: 3.0, // 3 seconds for setup + location: location, + participants: { home: null, away: null } + }; + console.log('Faceoff state set to:', this.faceoff); + this.emit('faceoff-start', { location }); + } + + updateFaceoff(deltaTime) { + if (!this.faceoff.isActive) return; + + this.faceoff.timeRemaining -= deltaTime; + + if (this.faceoff.timeRemaining <= 0) { + switch (this.faceoff.phase) { + case 'setup': + this.faceoff.phase = 'ready'; + this.faceoff.timeRemaining = 2.0; // 2 seconds ready phase + this.emit('faceoff-ready'); + break; + case 'ready': + this.faceoff.phase = 'drop'; + this.faceoff.timeRemaining = 0.5; // Brief drop phase + this.emit('faceoff-drop'); + break; + case 'drop': + this.completeFaceoff(); + break; + } + } + } + + completeFaceoff() { + this.faceoff.isActive = false; + this.faceoff.phase = 'complete'; + this.emit('faceoff-complete', { + winner: this.determineFaceoffWinner(), + location: this.faceoff.location + }); + } + + determineFaceoffWinner() { + const { home, away } = this.faceoff.participants; + if (!home || !away) return 'home'; // Default fallback + + // Simple skill-based determination with some randomness + const homeSkill = home.attributes.puckHandling + home.attributes.awareness; + const awaySkill = away.attributes.puckHandling + away.attributes.awareness; + + const homeProbability = homeSkill / (homeSkill + awaySkill); + const winner = Math.random() < homeProbability ? 'home' : 'away'; + + this.stats[winner].faceoffWins++; + return winner; + } + getGameState() { return { period: this.period, @@ -202,7 +271,8 @@ class GameState { isPaused: this.isPaused, gameSpeed: this.gameSpeed, gameOver: this.gameOver, - powerPlay: { ...this.powerPlay } + powerPlay: { ...this.powerPlay }, + faceoff: { ...this.faceoff } }; } } \ No newline at end of file diff --git a/src/engine/main.js b/src/engine/main.js index 1224c71..85fc15a 100644 --- a/src/engine/main.js +++ b/src/engine/main.js @@ -142,10 +142,8 @@ document.addEventListener('DOMContentLoaded', async () => { players = hockeyManager.gameEngine.players; - // Initial faceoff to start the game - setTimeout(() => { - hockeyManager.gameEngine.startFaceoff(); - }, 2000); + // Initial faceoff to start the game immediately + hockeyManager.gameEngine.startFaceoff(); console.log('Hockey Manager 2D Match Engine loaded successfully!'); console.log('Controls:'); diff --git a/src/entities/player.js b/src/entities/player.js index 4c15ab8..d47d913 100644 --- a/src/entities/player.js +++ b/src/entities/player.js @@ -116,6 +116,13 @@ class Player { this.aiState.lastAction = currentTime; + // Handle faceoff positioning + if (gameState.faceoff && gameState.faceoff.isActive) { + console.log(`Player ${this.name} (${this.role}) in faceoff mode, phase: ${gameState.faceoff.phase}`); + this.handleFaceoffPositioning(gameState, players); + return; + } + 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); @@ -352,4 +359,72 @@ class Player { ctx.restore(); } + + handleFaceoffPositioning(gameState, players) { + const faceoffPos = this.getFaceoffPosition(gameState, players); + this.moveToPosition(faceoffPos); + this.aiState.behavior = 'faceoff'; + + // Set faceoff participants for centers + if (this.role === 'C') { + const participantKey = this.team; + if (!gameState.faceoff.participants[participantKey]) { + gameState.faceoff.participants[participantKey] = this; + } + } + } + + getFaceoffPosition(gameState, players) { + const faceoffLocation = gameState.faceoff.location; + const side = this.team === 'home' ? -1 : 1; + const faceoffRadius = 50; // Radius of faceoff circle + + switch (this.role) { + case 'C': + // Centers line up directly at the faceoff dot, facing each other + return new Vector2( + faceoffLocation.x + side * 15, // Slight offset for positioning + faceoffLocation.y + ); + + case 'LW': + // Left wing must stay outside the faceoff circle + // Position them further back and outside the circle + return new Vector2( + faceoffLocation.x + side * (faceoffRadius + 30), // Outside circle + buffer + faceoffLocation.y - (faceoffRadius + 20) // Outside circle on left side + ); + + case 'RW': + // Right wing must stay outside the faceoff circle + // Position them further back and outside the circle + return new Vector2( + faceoffLocation.x + side * (faceoffRadius + 30), // Outside circle + buffer + faceoffLocation.y + (faceoffRadius + 20) // Outside circle on right side + ); + + case 'LD': + // Left defense well outside the faceoff area + return new Vector2( + faceoffLocation.x + side * (faceoffRadius + 80), + faceoffLocation.y - (faceoffRadius + 40) + ); + + case 'RD': + // Right defense well outside the faceoff area + return new Vector2( + faceoffLocation.x + side * (faceoffRadius + 80), + faceoffLocation.y + (faceoffRadius + 40) + ); + + case 'G': + // Goalies stay in their nets during faceoffs + return this.team === 'home' ? + new Vector2(50, gameState.rink.centerY) : + new Vector2(gameState.rink.width - 50, gameState.rink.centerY); + + default: + return this.homePosition; + } + } } \ No newline at end of file diff --git a/src/entities/puck.js b/src/entities/puck.js index 81514eb..d4970c9 100644 --- a/src/entities/puck.js +++ b/src/entities/puck.js @@ -199,13 +199,45 @@ class Puck { this.trail = []; } + faceoffDrop(winningTeam, location, participants) { + // Position puck at faceoff location + this.position = new Vector2(location.x, location.y); + this.velocity = new Vector2(0, 0); + + // Clear any existing puck possession + Object.values(participants).forEach(player => { + if (player) player.state.hasPuck = false; + }); + + const winner = participants[winningTeam]; + if (winner) { + // Puck moves backward toward the winning team's end + const direction = winningTeam === 'home' ? + new Vector2(-1, (Math.random() - 0.5) * 0.3) : // Home team shoots left to right, so backward is left + new Vector2(1, (Math.random() - 0.5) * 0.3); // Away team shoots right to left, so backward is right + + // Initial puck movement from faceoff + this.velocity = direction.normalize().multiply(80 + Math.random() * 40); + this.lastPlayerTouch = winner; + this.lastTeamTouch = winningTeam; + + // Winner doesn't immediately have possession - they have to chase the puck + setTimeout(() => { + // Only give possession if the puck is still near the winner + if (winner.position.distance(this.position) < 30) { + winner.state.hasPuck = true; + } + }, 200); + } + } + + // Legacy method for compatibility 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;