faceoff mechanics

This commit is contained in:
Pierre Wessman 2025-09-16 11:49:47 +02:00
parent 4c48e237c0
commit f1b511be15
5 changed files with 215 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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