class Player { /** * Creates a hockey player with physics properties, AI behavior, and game attributes * @param {string} id - Unique identifier for the player * @param {string} name - Player's display name * @param {string} team - Team affiliation ('home' or 'away') * @param {string} position - Hockey position ('LW', 'C', 'RW', 'LD', 'RD', 'G') * @param {number} x - Initial x coordinate * @param {number} y - Initial y coordinate */ 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' ? 200 : 280; 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, 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; } /** * Main update loop for the player - handles movement, rotation, and AI behavior * @param {number} deltaTime - Time elapsed since last frame in seconds * @param {Object} gameState - Current game state including rink dimensions and game status * @param {Object} puck - Puck object with position and velocity * @param {Array} players - Array of all players on the ice */ update(deltaTime, gameState, puck, players) { this.updateMovement(deltaTime); this.updateAngle(deltaTime); if (this.role !== 'G') { this.updateAI(gameState, puck, players); } else { this.updateGoalie(gameState, puck, players); } } /** * Updates player physics including movement toward target, velocity limits, and collision bounds * Applies acceleration toward target position with deceleration when close * @param {number} deltaTime - Time elapsed since last frame in seconds */ 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); } this.velocity = this.velocity.limit(this.maxSpeed); // 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(); } /** * Updates player rotation to face target angle with smooth turning animation * @param {number} deltaTime - Time elapsed since last frame in seconds */ 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); } } /** * Main AI decision making system - handles reaction timing, faceoffs, and behavioral switching * Delegates to specific behavior methods based on team puck possession * @param {Object} gameState - Current game state including faceoff status * @param {Object} puck - Puck object with position and velocity * @param {Array} players - Array of all players on the ice */ 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) { 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); // Determine team possession const teammatePuckCarrier = teammates.find(p => p.state.hasPuck); const opponentPuckCarrier = opponents.find(p => p.state.hasPuck); const myTeamHasPuck = this.state.hasPuck || teammatePuckCarrier; if (myTeamHasPuck) { this.behaviorWhenTeamHasPuck(gameState, puck, teammates, opponents, distanceToPuck); } else if (opponentPuckCarrier) { this.behaviorWhenOpponentHasPuck(gameState, puck, teammates, opponents, opponentPuckCarrier); } else { this.behaviorWhenPuckIsLoose(gameState, puck, teammates, opponents, distanceToPuck); } } /** * AI behavior when this player's team has possession of the puck * Handles both when this player has the puck and when a teammate has it * @param {Object} gameState - Current game state with rink dimensions * @param {Object} puck - Puck object with position and velocity * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opposing player objects * @param {number} distanceToPuck - Pre-calculated distance to puck */ behaviorWhenTeamHasPuck(gameState, puck, teammates, opponents, distanceToPuck) { if (this.state.hasPuck) { // This player has the puck - offensive behavior this.offensiveBehaviorWithPuck(gameState, puck, teammates, opponents); } else { // Teammate has the puck - support behavior this.supportOffensiveBehavior(gameState, puck, teammates, opponents); } } /** * AI behavior when the opponent team has possession of the puck * All players focus on defensive positioning and pressure * @param {Object} gameState - Current game state * @param {Object} puck - Puck object with position * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opposing player objects * @param {Object} opponentPuckCarrier - The opponent player who has the puck */ behaviorWhenOpponentHasPuck(gameState, puck, teammates, opponents, opponentPuckCarrier) { // Check if this player is the closest defender to the puck carrier const isClosestDefender = this.isClosestDefenderToPuckCarrier(opponentPuckCarrier, teammates); if (isClosestDefender) { // Closest defender aggressively targets the puck carrier this.moveToPosition(opponentPuckCarrier.position); this.aiState.behavior = 'aggressive_pressure'; } else { // Other players position defensively this.defendPosition(gameState, opponentPuckCarrier); } } /** * AI behavior when the puck is loose (no one has possession) * Players compete to gain control while maintaining team structure * @param {Object} gameState - Current game state * @param {Object} puck - Puck object with position * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opposing player objects * @param {number} distanceToPuck - Pre-calculated distance to puck */ behaviorWhenPuckIsLoose(gameState, puck, teammates, opponents, distanceToPuck) { const isClosestToPuck = this.isClosestPlayerToPuck(puck, teammates); const allPlayers = [...teammates, ...opponents, this]; if (isClosestToPuck && distanceToPuck < 200) { // Only chase if this player is closest to the puck on their team this.chasePuck(puck); } else { // Maintain formation position while puck is contested this.moveToFormationPosition(gameState, puck, allPlayers); } } /** * Offensive behavior when this specific player has the puck * Prioritizes shooting, then passing under pressure, then advancing toward goal * @param {Object} gameState - Current game state with rink dimensions * @param {Object} puck - Puck object to shoot or pass * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opposing player objects */ offensiveBehaviorWithPuck(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; } } // Check if any teammate is closer to goal for a better scoring position const teammateCloserToGoal = this.findTeammateCloserToGoal(teammates, opponents, enemyGoal, distanceToGoal); if (teammateCloserToGoal && Math.random() < 0.7) { this.pass(puck, teammateCloserToGoal); return; } // If under heavy pressure, look for a pass first if (distanceToNearestOpponent < 100) { 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); } /** * Support behavior when a teammate has the puck * Positions for passes, creates scoring opportunities, and maintains offensive formation * @param {Object} gameState - Current game state * @param {Object} puck - Puck object with position * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opposing player objects */ supportOffensiveBehavior(gameState, puck, teammates, opponents) { const allPlayers = [...teammates, ...opponents, this]; const puckCarrier = teammates.find(p => p.state.hasPuck); // Move to an offensive formation position that supports the puck carrier const supportPosition = this.getOffensiveSupportPosition(gameState, puck, puckCarrier, opponents); this.moveToPosition(supportPosition); this.aiState.behavior = 'offensive_support'; } /** * Goalie-specific AI behavior - stays in crease and tracks puck movement * Positions between puck and goal, with more aggressive positioning when puck is close * @param {Object} gameState - Current game state with rink dimensions * @param {Object} puck - Puck object with position * @param {Array} players - Array of all players (unused but maintained for consistency) */ updateGoalie(gameState, puck, players) { const goalXOffset = gameState.renderer?.goalXOffset || 80; // Fallback to 80 if renderer not available const goal = this.team === 'home' ? new Vector2(goalXOffset, gameState.rink.centerY) : new Vector2(gameState.rink.width - goalXOffset, 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)); } /** * Sets player target to chase after a loose puck * @param {Object} puck - Puck object with position to chase */ chasePuck(puck) { this.moveToPosition(puck.position); this.aiState.behavior = 'chasing'; } /** * Shoots the puck toward a target with power and accuracy based on player attributes * Applies random spread based on shooting accuracy - better shooters are more precise * @param {Object} puck - Puck object to shoot * @param {Vector2} target - Target position to aim for * @returns {boolean} True if shot was taken */ 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; } /** * Passes the puck to a teammate with power scaled by distance * Closer passes are softer, longer passes are harder * @param {Object} puck - Puck object to pass * @param {Object} target - Target player object to pass to * @returns {boolean} True if pass was made */ pass(puck, target) { const direction = target.position.subtract(puck.position).normalize(); const distance = puck.position.distance(target.position); const power = Math.min(800, distance * 2.5); puck.velocity = direction.multiply(power); this.state.hasPuck = false; return true; } /** * Attempts to body check an opponent player * If close enough, applies knockback force; otherwise moves toward target * @param {Object} target - Target player to check * @returns {boolean} True if check was successful (contact made) */ 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; } /** * Sets the player's target position and facing angle * @param {Vector2} target - Target position to move toward */ moveToPosition(target) { this.targetPosition = target.copy(); this.targetAngle = target.subtract(this.position).angle(); } /** * Positions player defensively between opponent and own goal * Uses interpolation to stay closer to opponent than goal * @param {Object} gameState - Current game state with rink dimensions * @param {Object} opponent - Opponent player to defend against */ 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'; } /** * Moves player to their calculated formation position based on game context * @param {Object} gameState - Current game state * @param {Object} puck - Puck object with position * @param {Array} players - Array of all players for formation calculation */ moveToFormationPosition(gameState, puck, players) { this.moveToPosition(this.getFormationPosition(gameState, puck, players)); this.aiState.behavior = 'formation'; } /** * Calculates ideal formation position for this player based on team state and puck location * @param {Object} gameState - Current game state with rink dimensions * @param {Object} puck - Puck object with position * @param {Array} players - Array of all players to determine puck ownership * @returns {Vector2} Calculated formation position */ 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); } /** * Calculates offensive support position when a teammate has the puck * Positions player to receive passes, create scoring chances, and maintain offensive pressure * @param {Object} gameState - Current game state with rink dimensions * @param {Object} puck - Puck object with position * @param {Object} puckCarrier - Teammate who has the puck * @param {Array} opponents - Array of opposing player objects * @returns {Vector2} Calculated offensive support position */ getOffensiveSupportPosition(gameState, puck, puckCarrier, opponents) { const rink = gameState.rink; const enemyGoal = this.team === 'home' ? new Vector2(rink.width - 50, rink.centerY) : new Vector2(50, rink.centerY); // Base position is an aggressive offensive formation const side = this.team === 'home' ? 1 : -1; const attackZone = this.team === 'home' ? rink.width * 0.75 : rink.width * 0.25; // Away team players need their left/right flipped since they face the opposite direction const leftSideY = this.team === 'home' ? -140 : 140; const rightSideY = this.team === 'home' ? 140 : -140; const leftDefenseY = this.team === 'home' ? -100 : 100; const rightDefenseY = this.team === 'home' ? 100 : -100; let baseX, baseY; switch (this.role) { case 'C': // Center positions for rebounds and passes baseX = attackZone; baseY = rink.centerY + (puck.position.y > rink.centerY ? -60 : 60); break; case 'LW': // Left wing pushes forward on their side for passing options baseX = attackZone + 40; // Push forward past attack zone baseY = rink.centerY + leftSideY; break; case 'RW': // Right wing pushes forward on their side for passing options baseX = attackZone + 40; // Push forward past attack zone baseY = rink.centerY + rightSideY; break; case 'LD': // Left defense supports from the point baseX = attackZone - 120; baseY = rink.centerY + leftDefenseY; break; case 'RD': // Right defense supports from the point baseX = attackZone - 120; baseY = rink.centerY + rightDefenseY; break; default: return this.getFormationPosition(gameState, puck, [puckCarrier, ...opponents, this]); } // Adjust position to avoid clustering with puck carrier if (puckCarrier) { const distanceToPuckCarrier = new Vector2(baseX, baseY).distance(puckCarrier.position); if (distanceToPuckCarrier < 80) { // Spread out from puck carrier const awayFromCarrier = new Vector2(baseX, baseY).subtract(puckCarrier.position).normalize(); baseX += awayFromCarrier.x * 50; baseY += awayFromCarrier.y * 50; } } // 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); } /** * Determines if this player's team is in attacking or defending mode * Based on puck possession and puck location on the rink * @param {Object} puck - Puck object with position * @param {Object} puckOwner - Player object who has puck possession (null if loose) * @param {Object} rink - Rink object with dimensions * @returns {boolean} True if team is attacking, false if defending */ 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; } } /** * Calculates specific position for player based on role, team state, and puck influence * Different formations for attacking vs defending, with puck tracking adjustments * @param {Object} rink - Rink object with dimensions and center points * @param {number} side - Team side multiplier (-1 for home, 1 for away) * @param {boolean} isAttacking - Whether team is in attacking formation * @param {Object} puck - Puck object for positional influence * @returns {Vector2} Calculated contextual position */ 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) // Away team players need their left/right flipped since they face the opposite direction const leftSideY = this.team === 'home' ? -120 : 120; const rightSideY = this.team === 'home' ? 120 : -120; const leftDefenseY = this.team === 'home' ? -80 : 80; const rightDefenseY = this.team === 'home' ? 80 : -80; const leftDefensiveY = this.team === 'home' ? -100 : 100; const rightDefensiveY = this.team === 'home' ? 100 : -100; const leftDefensiveDefenseY = this.team === 'home' ? -60 : 60; const rightDefensiveDefenseY = this.team === 'home' ? 60 : -60; 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 + leftSideY; break; case 'RW': baseX = attackZone - 50; baseY = centerY + rightSideY; break; case 'LD': baseX = attackZone - 150; baseY = centerY + leftDefenseY; break; case 'RD': baseX = attackZone - 150; baseY = centerY + rightDefenseY; 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 + leftDefensiveY; break; case 'RW': baseX = defenseZone + side * 50; baseY = centerY + rightDefensiveY; break; case 'LD': baseX = defenseZone + side * 100; baseY = centerY + leftDefensiveDefenseY; break; case 'RD': baseX = defenseZone + side * 100; baseY = centerY + rightDefensiveDefenseY; 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); } /** * Finds the closest player from a given array of players * @param {Array} players - Array of player objects to search through * @returns {Object|null} Nearest player object, or null if no players provided */ 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; } /** * Evaluates teammates to find the best pass target based on distance, skill, and opponent blocking * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opponent players that might block the pass * @returns {Object|null} Best teammate to pass to, or null if no good options */ 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; } /** * Finds a teammate who is closer to the goal than the current puck carrier * and has a clear passing lane for better scoring opportunities * @param {Array} teammates - Array of teammate player objects * @param {Array} opponents - Array of opponent players that might block the pass * @param {Vector2} enemyGoal - Position of the opponent's goal * @param {number} myDistanceToGoal - Current player's distance to goal * @returns {Object|null} Best teammate closer to goal, or null if no good options */ findTeammateCloserToGoal(teammates, opponents, enemyGoal, myDistanceToGoal) { let bestTarget = null; let bestScore = -1; teammates.forEach(teammate => { // Skip goalies if (teammate.role === 'G') return; const teammateDistanceToGoal = teammate.position.distance(enemyGoal); const distanceToTeammate = this.position.distance(teammate.position); // Only consider teammates who are closer to goal and within reasonable passing distance if (teammateDistanceToGoal >= myDistanceToGoal || distanceToTeammate < 50 || distanceToTeammate > 250) { return; } // Check if pass is blocked by opponents 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.4 && this.position.distance(opponent.position) < distanceToTeammate) { blocked = true; } }); if (!blocked) { // Score based on how much closer to goal they are and their attributes const goalAdvantage = myDistanceToGoal - teammateDistanceToGoal; const score = (goalAdvantage * teammate.attributes.shooting) / distanceToTeammate; if (score > bestScore) { bestScore = score; bestTarget = teammate; } } }); return bestTarget; } /** * Determines if this player is the closest non-goalie teammate to the puck * Used to decide who should chase loose pucks * @param {Object} puck - Puck object with position * @param {Array} teammates - Array of teammate player objects * @returns {boolean} True if this player is closest to puck on their team */ 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; } /** * Checks if this player is the closest defender to the puck carrier * @param {Object} puckCarrier - The player who has the puck * @param {Array} teammates - Array of teammate player objects * @returns {boolean} True if this player is closest to puck carrier on their team */ isClosestDefenderToPuckCarrier(puckCarrier, teammates) { // Skip goalies if (this.role === 'G') return false; const myDistance = this.position.distance(puckCarrier.position); // Include self in the list to compare against (excluding goalies) const allTeamPlayers = [this, ...teammates.filter(t => t.role !== 'G')]; // Find the closest player to the puck carrier let closestDistance = Infinity; let closestPlayer = null; allTeamPlayers.forEach(player => { const distance = player.position.distance(puckCarrier.position); if (distance < closestDistance) { closestDistance = distance; closestPlayer = player; } }); return closestPlayer === this; } /** * Evaluates whether player has a clear shooting angle to goal * Checks if opponents are blocking the direct path to goal * @param {Vector2} goalPosition - Target goal position * @param {Array} opponents - Array of opponent players that might block shot * @returns {boolean} True if shooting angle is clear */ 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; } /** * Intelligently advances player with puck toward the opponent's goal * Uses pathfinding to avoid opponents and direct approach when close * @param {Vector2} goalPosition - Target goal position to advance toward * @param {Array} opponents - Array of opponent players to avoid * @param {Object} rink - Rink object with boundary dimensions */ 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); } /** * Calculates path adjustments to avoid opponents while advancing toward goal * Creates lateral movement to navigate around blocking opponents * @param {Vector2} goalPosition - Target goal position * @param {Array} opponents - Array of opponent players to avoid * @param {Object} rink - Rink object for boundary awareness * @returns {Vector2} Position adjustment vector to avoid opponents */ 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; } /** * Constrains player position to stay within rink boundaries * Uses hardcoded rink dimensions of 1000x600 */ 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)); } /** * Renders the player on the canvas with team colors, puck indicator, and role text * @param {CanvasRenderingContext2D} ctx - 2D rendering context for drawing */ 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 = this.state.hasPuck ? '#000' : '#fff'; ctx.lineWidth = this.state.hasPuck ? 3 : 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(); } /** * Handles player positioning during faceoff situations * Centers participate directly, other positions maintain legal distance from faceoff circle * @param {Object} gameState - Current game state with faceoff information * @param {Array} players - Array of all players for positioning context */ 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; } } } /** * Calculates legal faceoff positioning for each player role * Centers face off directly, other positions must stay outside faceoff circle per hockey rules * @param {Object} gameState - Current game state with faceoff location and rink info * @param {Array} players - Array of all players (unused but maintained for consistency) * @returns {Vector2} Legal faceoff position for this player's role */ getFaceoffPosition(gameState, players) { const faceoffLocation = gameState.faceoff.location; const side = this.team === 'home' ? -1 : 1; const faceoffRadius = RINK_CIRCLES.FACEOFF_CIRCLE_RADIUS; // Radius of faceoff circle // Away team players need their left/right flipped since they face the opposite direction const leftWingY = this.team === 'home' ? -(faceoffRadius + 20) : (faceoffRadius + 20); const rightWingY = this.team === 'home' ? (faceoffRadius + 20) : -(faceoffRadius + 20); const leftDefenseY = this.team === 'home' ? -(faceoffRadius + 40) : (faceoffRadius + 40); const rightDefenseY = this.team === 'home' ? (faceoffRadius + 40) : -(faceoffRadius + 40); 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 + leftWingY // 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 + rightWingY // Outside circle on right side ); case 'LD': // Left defense well outside the faceoff area return new Vector2( faceoffLocation.x + side * (faceoffRadius + 80), faceoffLocation.y + leftDefenseY ); case 'RD': // Right defense well outside the faceoff area return new Vector2( faceoffLocation.x + side * (faceoffRadius + 80), faceoffLocation.y + rightDefenseY ); 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; } } }