Pierre Wessman 1ea9657aa4 Improve offensive AI: wingers push forward and better passing logic
- Wingers now push forward past attack zone (+40 units) when team has possession to create passing options
- Added findTeammateCloserToGoal() method to prioritize passes to teammates in better scoring positions
- Puck handlers now pass to teammates closer to goal with 70% probability
- Enhanced team coordination and more realistic offensive positioning

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 20:51:47 +02:00

1041 lines
42 KiB
JavaScript

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,
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;
}
/**
* Main update loop for the player - handles energy, 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.updateEnergy(deltaTime);
this.updateMovement(deltaTime);
this.updateAngle(deltaTime);
if (this.role !== 'G') {
this.updateAI(gameState, puck, players);
} else {
this.updateGoalie(gameState, puck, players);
}
}
/**
* Updates player energy/stamina based on movement and provides recovery when stationary
* Energy affects max speed - tired players move slower
* @param {number} deltaTime - Time elapsed since last frame in seconds
*/
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);
}
}
/**
* 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);
}
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();
}
/**
* 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;
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 - 140;
break;
case 'RW':
// Right wing pushes forward on their side for passing options
baseX = attackZone + 40; // Push forward past attack zone
baseY = rink.centerY + 140;
break;
case 'LD':
// Left defense supports from the point
baseX = attackZone - 120;
baseY = rink.centerY - 100;
break;
case 'RD':
// Right defense supports from the point
baseX = attackZone - 120;
baseY = rink.centerY + 100;
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)
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);
}
/**
* 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
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;
}
}
}