858 lines
33 KiB
JavaScript
858 lines
33 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 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) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Offensive AI behavior when player has possession of 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
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Defensive AI behavior when player doesn't have puck possession
|
|
* Chooses between chasing loose puck, defending against opponents, or maintaining 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
|
|
* @param {number} distanceToPuck - Pre-calculated distance to puck
|
|
*/
|
|
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) {
|
|
// Check if this player is the closest defender to the puck carrier
|
|
const isClosestDefender = this.isClosestDefenderToPuckCarrier(puckOwner, teammates);
|
|
|
|
if (isClosestDefender) {
|
|
// Closest defender aggressively targets the puck carrier
|
|
this.moveToPosition(puckOwner.position);
|
|
this.aiState.behavior = 'aggressive_pressure';
|
|
} else if (distanceToPuck < 150 && Math.random() < 0.2) {
|
|
this.checkPlayer(puckOwner);
|
|
} else {
|
|
this.defendPosition(gameState, puckOwner);
|
|
}
|
|
} else {
|
|
this.moveToFormationPosition(gameState, puck, allPlayers);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 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));
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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 = 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;
|
|
}
|
|
}
|
|
} |