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