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, 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) { 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; // 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); 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); const distanceToNearestOpponent = nearestOpponent ? this.position.distance(nearestOpponent.position) : Infinity; // More aggressive shooting when in scoring position if (distanceToGoal < 250 && this.hasGoodShootingAngle(enemyGoal, opponents)) { if (distanceToGoal < 150 || Math.random() < 0.5) { this.shoot(puck, enemyGoal); return; } } // If under heavy pressure, look for a pass first if (distanceToNearestOpponent < 60) { const bestTeammate = this.findBestPassTarget(teammates, opponents); if (bestTeammate && Math.random() < 0.8) { this.pass(puck, bestTeammate); return; } } // Default behavior: advance aggressively toward goal this.advanceTowardGoal(enemyGoal, opponents, gameState.rink); } behaviorWithoutPuck(gameState, puck, teammates, opponents, distanceToPuck) { const puckOwner = opponents.find(p => p.state.hasPuck) || teammates.find(p => p.state.hasPuck); const isClosestToPuck = this.isClosestPlayerToPuck(puck, teammates); const allPlayers = [...teammates, ...opponents, this]; if (!puckOwner && isClosestToPuck && distanceToPuck < 200) { // Only chase if this player is closest to the puck on their team 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, puck, allPlayers); } } updateGoalie(gameState, puck, players) { const goal = this.team === 'home' ? new Vector2(10, gameState.rink.centerY) : new Vector2(gameState.rink.width - 10, 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, puck, players) { this.moveToPosition(this.getFormationPosition(gameState, puck, players)); this.aiState.behavior = 'formation'; } getFormationPosition(gameState, puck, players) { const side = this.team === 'home' ? -1 : 1; const rink = gameState.rink; // Determine if team is attacking or defending based on puck position and possession const puckOwner = players.find(p => p.state.hasPuck); const isAttacking = this.determineTeamState(puck, puckOwner, rink); // Get base formation position based on attacking/defending state return this.getContextualPosition(rink, side, isAttacking, puck); } determineTeamState(puck, puckOwner, rink) { const homeAttackingZone = rink.width * 0.67; // Right side for home team const awayAttackingZone = rink.width * 0.33; // Left side for away team // If teammate has puck, team is likely attacking if (puckOwner && puckOwner.team === this.team) { return true; } // If opponent has puck, team is defending if (puckOwner && puckOwner.team !== this.team) { return false; } // No possession - determine by puck location if (this.team === 'home') { return puck.position.x > homeAttackingZone; } else { return puck.position.x < awayAttackingZone; } } getContextualPosition(rink, side, isAttacking, puck) { const centerY = rink.centerY; const puckInfluenceX = (puck.position.x - rink.centerX) * 0.3; // Follow puck horizontally const puckInfluenceY = (puck.position.y - centerY) * 0.2; // Follow puck vertically (less influence) let baseX, baseY; if (isAttacking) { // Attacking formation - push forward toward opponent's goal const attackZone = this.team === 'home' ? rink.width * 0.75 : rink.width * 0.25; switch (this.role) { case 'C': baseX = attackZone; baseY = centerY; break; case 'LW': baseX = attackZone - 50; baseY = centerY - 120; break; case 'RW': baseX = attackZone - 50; baseY = centerY + 120; break; case 'LD': baseX = attackZone - 150; baseY = centerY - 80; break; case 'RD': baseX = attackZone - 150; baseY = centerY + 80; break; default: return this.homePosition; } } else { // Defensive formation - fall back toward own goal const defenseZone = this.team === 'home' ? rink.width * 0.25 : rink.width * 0.75; switch (this.role) { case 'C': baseX = defenseZone; baseY = centerY; break; case 'LW': baseX = defenseZone + side * 50; baseY = centerY - 100; break; case 'RW': baseX = defenseZone + side * 50; baseY = centerY + 100; break; case 'LD': baseX = defenseZone + side * 100; baseY = centerY - 60; break; case 'RD': baseX = defenseZone + side * 100; baseY = centerY + 60; break; default: return this.homePosition; } } // Apply puck influence for more dynamic positioning baseX += puckInfluenceX; baseY += puckInfluenceY; // Keep positions within rink bounds baseX = Math.max(50, Math.min(rink.width - 50, baseX)); baseY = Math.max(50, Math.min(rink.height - 50, baseY)); return new Vector2(baseX, baseY); } 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; } isClosestPlayerToPuck(puck, teammates) { // Check if this player (excluding goalies) is closest to the puck on their team if (this.role === 'G' || this.state.hasPuck) return false; const myDistance = this.position.distance(puck.position); // Include self in the list to compare against const allTeamPlayers = [this, ...teammates.filter(t => t.role !== 'G' && !t.state.hasPuck)]; // Find the closest player let closestDistance = Infinity; let closestPlayer = null; allTeamPlayers.forEach(player => { const distance = player.position.distance(puck.position); if (distance < closestDistance) { closestDistance = distance; closestPlayer = player; } }); return closestPlayer === this; } hasGoodShootingAngle(goalPosition, opponents) { // Check if there's a clear line to goal (simplified check) const directionToGoal = goalPosition.subtract(this.position).normalize(); // Check if opponents are blocking the shot for (let opponent of opponents) { const directionToOpponent = opponent.position.subtract(this.position); const distanceToOpponent = directionToOpponent.magnitude(); // Skip distant opponents if (distanceToOpponent > 150) continue; const directionToOpponentNorm = directionToOpponent.normalize(); const dot = directionToGoal.dot(directionToOpponentNorm); // If opponent is roughly in line with goal and close enough to block if (dot > 0.8 && distanceToOpponent < 80) { return false; } } return true; } advanceTowardGoal(goalPosition, opponents, rink) { // Create an intelligent path toward the goal let targetPosition = goalPosition.copy(); // Adjust approach based on position and opponents const directionToGoal = goalPosition.subtract(this.position).normalize(); const distanceToGoal = this.position.distance(goalPosition); // If close to goal, move more directly if (distanceToGoal < 200) { this.moveToPosition(goalPosition); return; } // Look for the best path around opponents const pathAdjustment = this.findBestPathToGoal(goalPosition, opponents, rink); targetPosition = targetPosition.add(pathAdjustment); // Keep target in bounds targetPosition.x = Math.max(50, Math.min(rink.width - 50, targetPosition.x)); targetPosition.y = Math.max(50, Math.min(rink.height - 50, targetPosition.y)); this.moveToPosition(targetPosition); } findBestPathToGoal(goalPosition, opponents, rink) { const currentPos = this.position; const adjustment = new Vector2(0, 0); // Check for opponents blocking direct path const directPath = goalPosition.subtract(currentPos).normalize(); opponents.forEach(opponent => { const toOpponent = opponent.position.subtract(currentPos); const distanceToOpponent = toOpponent.magnitude(); // Only consider opponents that might interfere if (distanceToOpponent > 120 || distanceToOpponent < 30) return; const toOpponentNorm = toOpponent.normalize(); const dot = directPath.dot(toOpponentNorm); // If opponent is somewhat in the path if (dot > 0.5) { // Calculate avoidance vector (perpendicular to opponent direction) const avoidVector = new Vector2(-toOpponentNorm.y, toOpponentNorm.x); const influence = (120 - distanceToOpponent) / 120; // Stronger influence when closer // Choose direction based on field position to avoid going out of bounds if (currentPos.y < rink.height / 2) { adjustment.y += Math.abs(avoidVector.y) * influence * 30; } else { adjustment.y -= Math.abs(avoidVector.y) * influence * 30; } // Slight lateral adjustment adjustment.x += avoidVector.x * influence * 15; } }); return adjustment; } 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(); } 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; } } }