Compare commits

...

3 Commits

Author SHA1 Message Date
Pierre Wessman
8e68202b23 . 2025-09-19 13:19:38 +02:00
Pierre Wessman
66c949e47d Fix goalie collision on failed saves by adding 500ms cooldown
When goalies fail to save shots, they now have a 500ms collision cooldown
to prevent the puck from bouncing off them repeatedly. This allows the
puck to continue into the goal after a failed save.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 13:19:01 +02:00
Pierre Wessman
15c24608d4 Fix goal detection to match light red goal areas
- Update isInGoal() to check actual goal area boundaries (goalXOffset ± goalDepth)
- Update handleGoal() to use consistent goal area coordinates
- Add continuous goal checking in puck update loop
- Add goal post collision detection to prevent entry from sides/behind
- Goals now only register when puck enters light red areas from the front

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 13:14:43 +02:00
2 changed files with 222 additions and 9 deletions

View File

@ -49,6 +49,16 @@ class Player {
this.homePosition = new Vector2(x, y); this.homePosition = new Vector2(x, y);
this.angle = 0; this.angle = 0;
this.targetAngle = 0; this.targetAngle = 0;
// Stuck detection and pathfinding
this.stuckDetection = {
lastPosition: new Vector2(x, y),
stuckTimer: 0,
isStuck: false,
alternativeTarget: null,
stuckThreshold: 500, // 500ms
movementThreshold: 5 // minimum movement distance to not be considered stuck
};
} }
/** /**
@ -72,12 +82,21 @@ class Player {
/** /**
* Updates player physics including movement toward target, velocity limits, and collision bounds * Updates player physics including movement toward target, velocity limits, and collision bounds
* Applies acceleration toward target position with deceleration when close * Applies acceleration toward target position with deceleration when close
* Includes stuck detection and alternative pathfinding when blocked
* @param {number} deltaTime - Time elapsed since last frame in seconds * @param {number} deltaTime - Time elapsed since last frame in seconds
* @param {Object} gameState - Current game state for boundary checking * @param {Object} gameState - Current game state for boundary checking
*/ */
updateMovement(deltaTime, gameState) { updateMovement(deltaTime, gameState) {
const direction = this.targetPosition.subtract(this.position).normalize(); // Update stuck detection before movement
const distance = this.position.distance(this.targetPosition); this.updateStuckDetection(deltaTime);
// Choose target - either original or alternative if stuck
const currentTarget = this.stuckDetection.isStuck && this.stuckDetection.alternativeTarget
? this.stuckDetection.alternativeTarget
: this.targetPosition;
const direction = currentTarget.subtract(this.position).normalize();
const distance = this.position.distance(currentTarget);
if (distance > 10) { if (distance > 10) {
const force = direction.multiply(this.acceleration * deltaTime); const force = direction.multiply(this.acceleration * deltaTime);
@ -85,6 +104,11 @@ class Player {
} else { } else {
// Slow down when close to target // Slow down when close to target
this.velocity = this.velocity.multiply(0.8); this.velocity = this.velocity.multiply(0.8);
// If we reached alternative target, clear stuck state
if (this.stuckDetection.isStuck && this.stuckDetection.alternativeTarget) {
this.clearStuckState();
}
} }
this.velocity = this.velocity.limit(this.maxSpeed); this.velocity = this.velocity.limit(this.maxSpeed);
@ -112,6 +136,98 @@ class Player {
} }
} }
/**
* Updates stuck detection system - tracks if player hasn't moved significantly
* and triggers alternative pathfinding when stuck for more than threshold time
* @param {number} deltaTime - Time elapsed since last frame in seconds
*/
updateStuckDetection(deltaTime) {
const currentPosition = this.position;
const deltaTimeMs = deltaTime * 1000;
// Calculate movement since last position check
const movementDistance = currentPosition.distance(this.stuckDetection.lastPosition);
if (movementDistance < this.stuckDetection.movementThreshold) {
// Player hasn't moved much, increment stuck timer
this.stuckDetection.stuckTimer += deltaTimeMs;
if (this.stuckDetection.stuckTimer >= this.stuckDetection.stuckThreshold) {
if (!this.stuckDetection.isStuck) {
// Player just became stuck, find alternative path
this.handleStuckState();
}
}
} else {
// Player moved significantly, reset stuck detection
this.stuckDetection.stuckTimer = 0;
if (this.stuckDetection.isStuck) {
this.clearStuckState();
}
this.stuckDetection.lastPosition = currentPosition.copy();
}
}
/**
* Handles when a player becomes stuck - finds alternative pathfinding target
* Creates waypoint around obstacles to reach the original target
*/
handleStuckState() {
this.stuckDetection.isStuck = true;
// Find alternative target by trying different angles around the obstacle
const originalTarget = this.targetPosition;
const currentPos = this.position;
const directionToTarget = originalTarget.subtract(currentPos).normalize();
// Try different angles (45°, 90°, -45°, -90°) to find a clear path
const angles = [Math.PI / 4, Math.PI / 2, -Math.PI / 4, -Math.PI / 2];
const wayPointDistance = 60; // Distance to create waypoint
for (let angle of angles) {
const rotatedDirection = this.rotateVector(directionToTarget, angle);
const wayPoint = currentPos.add(rotatedDirection.multiply(wayPointDistance));
// Simple bounds check for waypoint
if (wayPoint.x > 50 && wayPoint.x < 950 &&
wayPoint.y > 50 && wayPoint.y < 550) {
this.stuckDetection.alternativeTarget = wayPoint;
break;
}
}
// If no good waypoint found, try backing up slightly
if (!this.stuckDetection.alternativeTarget) {
const backupDirection = directionToTarget.multiply(-1);
this.stuckDetection.alternativeTarget = currentPos.add(backupDirection.multiply(30));
}
}
/**
* Clears stuck state and resets stuck detection variables
*/
clearStuckState() {
this.stuckDetection.isStuck = false;
this.stuckDetection.alternativeTarget = null;
this.stuckDetection.stuckTimer = 0;
this.stuckDetection.lastPosition = this.position.copy();
}
/**
* Rotates a 2D vector by the given angle
* @param {Vector2} vector - Vector to rotate
* @param {number} angle - Rotation angle in radians
* @returns {Vector2} Rotated vector
*/
rotateVector(vector, angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return new Vector2(
vector.x * cos - vector.y * sin,
vector.x * sin + vector.y * cos
);
}
/** /**
* Main AI decision making system - handles reaction timing, faceoffs, and behavioral switching * Main AI decision making system - handles reaction timing, faceoffs, and behavioral switching
* Delegates to specific behavior methods based on team puck possession * Delegates to specific behavior methods based on team puck possession

View File

@ -12,6 +12,9 @@ class Puck {
this.bounceCount = 0; this.bounceCount = 0;
this.trail = []; this.trail = [];
this.maxTrailLength = 10; this.maxTrailLength = 10;
// Collision cooldown tracking
this.goalieCollisionCooldowns = new Map(); // Maps player ID to cooldown end time
} }
update(deltaTime, gameState, players) { update(deltaTime, gameState, players) {
@ -20,6 +23,11 @@ class Puck {
this.checkPlayerCollisions(players, gameState); this.checkPlayerCollisions(players, gameState);
this.checkPuckPossession(players); this.checkPuckPossession(players);
this.updateTrail(); this.updateTrail();
// Check for goals continuously, not just on board collisions
if (this.isInGoal(gameState)) {
this.handleGoal(gameState);
}
} }
updatePosition(deltaTime) { updatePosition(deltaTime) {
@ -86,6 +94,24 @@ class Puck {
} }
} }
isGoalieOnCooldown(goalie) {
const playerId = goalie.id || `${goalie.team}-${goalie.role}`;
const cooldownEnd = this.goalieCollisionCooldowns.get(playerId);
if (cooldownEnd && Date.now() < cooldownEnd) {
return true;
}
// Clean up expired cooldowns
if (cooldownEnd && Date.now() >= cooldownEnd) {
this.goalieCollisionCooldowns.delete(playerId);
}
return false;
}
setGoalieCooldown(goalie, durationMs = 500) {
const playerId = goalie.id || `${goalie.team}-${goalie.role}`;
this.goalieCollisionCooldowns.set(playerId, Date.now() + durationMs);
}
checkBoardCollisions(gameState) { checkBoardCollisions(gameState) {
const rink = gameState.rink; const rink = gameState.rink;
let collision = false; let collision = false;
@ -152,7 +178,7 @@ class Puck {
collision = true; collision = true;
} }
// Check left goal side walls // Check left goal side walls and front posts
if (this.position.x >= leftGoalLeft && this.position.x <= leftGoalRight) { if (this.position.x >= leftGoalLeft && this.position.x <= leftGoalRight) {
// Top side wall // Top side wall
if (this.position.y - this.radius <= leftGoalTop && this.position.y >= leftGoalTop - 20) { if (this.position.y - this.radius <= leftGoalTop && this.position.y >= leftGoalTop - 20) {
@ -168,6 +194,22 @@ class Puck {
} }
} }
// Check left goal front posts (prevent entering from sides)
if (this.position.x + this.radius >= leftGoalRight && this.position.x <= leftGoalRight + 10) {
// Top post
if (this.position.y >= leftGoalTop - 10 && this.position.y <= leftGoalTop + 10) {
this.velocity.x = -Math.abs(this.velocity.x);
this.position.x = leftGoalRight - this.radius;
collision = true;
}
// Bottom post
if (this.position.y >= leftGoalBottom - 10 && this.position.y <= leftGoalBottom + 10) {
this.velocity.x = -Math.abs(this.velocity.x);
this.position.x = leftGoalRight - this.radius;
collision = true;
}
}
// Check right goal side walls // Check right goal side walls
if (this.position.x >= rightGoalLeft && this.position.x <= rightGoalRight) { if (this.position.x >= rightGoalLeft && this.position.x <= rightGoalRight) {
// Top side wall // Top side wall
@ -184,15 +226,45 @@ class Puck {
} }
} }
// Check right goal front posts (prevent entering from sides)
if (this.position.x - this.radius <= rightGoalLeft && this.position.x >= rightGoalLeft - 10) {
// Top post
if (this.position.y >= rightGoalTop - 10 && this.position.y <= rightGoalTop + 10) {
this.velocity.x = Math.abs(this.velocity.x);
this.position.x = rightGoalLeft + this.radius;
collision = true;
}
// Bottom post
if (this.position.y >= rightGoalBottom - 10 && this.position.y <= rightGoalBottom + 10) {
this.velocity.x = Math.abs(this.velocity.x);
this.position.x = rightGoalLeft + this.radius;
collision = true;
}
}
return collision; return collision;
} }
isInGoal(gameState) { isInGoal(gameState) {
const goalY = gameState.rink.centerY; const rink = gameState.rink;
const goalHeight = gameState.rink.goalHeight; const goalY = rink.centerY;
const goalHeight = rink.goalHeight;
const goalDepth = 25;
const goalXOffset = gameState.renderer?.goalXOffset || 80;
// Check if puck is in the vertical range of the goals
if (this.position.y >= goalY - goalHeight && this.position.y <= goalY + goalHeight) { if (this.position.y >= goalY - goalHeight && this.position.y <= goalY + goalHeight) {
if (this.position.x <= 20 || this.position.x >= gameState.rink.width - 20) { // Left goal (light red area)
const leftGoalRight = goalXOffset;
const leftGoalLeft = goalXOffset - goalDepth;
// Right goal (light red area)
const rightGoalLeft = rink.width - goalXOffset;
const rightGoalRight = rink.width - goalXOffset + goalDepth;
// Check if puck is inside either light red goal area
if ((this.position.x >= leftGoalLeft && this.position.x <= leftGoalRight) ||
(this.position.x >= rightGoalLeft && this.position.x <= rightGoalRight)) {
return true; return true;
} }
} }
@ -203,13 +275,27 @@ class Puck {
// Determine which goal was scored in and validate the direction // Determine which goal was scored in and validate the direction
let scoringTeam = null; let scoringTeam = null;
if (this.position.x <= 20) { const rink = gameState.rink;
const goalY = rink.centerY;
const goalHeight = rink.goalHeight;
const goalDepth = 25;
const goalXOffset = gameState.renderer?.goalXOffset || 80;
// Left goal (light red area)
const leftGoalRight = goalXOffset;
const leftGoalLeft = goalXOffset - goalDepth;
// Right goal (light red area)
const rightGoalLeft = rink.width - goalXOffset;
const rightGoalRight = rink.width - goalXOffset + goalDepth;
if (this.position.x >= leftGoalLeft && this.position.x <= leftGoalRight) {
// Puck is in the LEFT goal (home team's goal) // Puck is in the LEFT goal (home team's goal)
// Only count as goal if puck came from the right side (positive x velocity) // Only count as goal if puck came from the right side (positive x velocity)
if (this.velocity.x > 0 || (this.lastTeamTouch === 'away')) { if (this.velocity.x > 0 || (this.lastTeamTouch === 'away')) {
scoringTeam = 'away'; // Away team scored on home team's goal scoringTeam = 'away'; // Away team scored on home team's goal
} }
} else if (this.position.x >= gameState.rink.width - 20) { } else if (this.position.x >= rightGoalLeft && this.position.x <= rightGoalRight) {
// Puck is in the RIGHT goal (away team's goal) // Puck is in the RIGHT goal (away team's goal)
// Only count as goal if puck came from the left side (negative x velocity) // Only count as goal if puck came from the left side (negative x velocity)
if (this.velocity.x < 0 || (this.lastTeamTouch === 'home')) { if (this.velocity.x < 0 || (this.lastTeamTouch === 'home')) {
@ -237,6 +323,11 @@ class Puck {
let closestDistance = Infinity; let closestDistance = Infinity;
players.forEach(player => { players.forEach(player => {
// Skip goalies who are on collision cooldown
if (player.role === 'G' && this.isGoalieOnCooldown(player)) {
return;
}
const distance = this.position.distance(player.position); const distance = this.position.distance(player.position);
const collisionDistance = this.radius + player.radius; const collisionDistance = this.radius + player.radius;
@ -327,10 +418,13 @@ class Puck {
difficulty: difficulty difficulty: difficulty
}); });
} else { } else {
console.log("Failed save") console.log("Failed save - setting collision cooldown")
// Failed save - puck continues with reduced velocity (already bounced from collision) // Failed save - puck continues with reduced velocity (already bounced from collision)
// The collision physics have already been applied, so the puck will continue toward the goal // The collision physics have already been applied, so the puck will continue toward the goal
this.velocity = this.velocity.multiply(0.8); // Slightly reduce speed but let it continue this.velocity = this.velocity.multiply(0.8); // Slightly reduce speed but let it continue
// Set collision cooldown to prevent immediate re-collision with goalie
this.setGoalieCooldown(goalie, 500); // 500ms cooldown
} }
} }
@ -352,6 +446,9 @@ class Puck {
this.lastTeamTouch = null; this.lastTeamTouch = null;
this.bounceCount = 0; this.bounceCount = 0;
this.trail = []; this.trail = [];
// Clear all goalie collision cooldowns
this.goalieCollisionCooldowns.clear();
} }
faceoffDrop(winningTeam, location, participants) { faceoffDrop(winningTeam, location, participants) {