Improve goalie save mechanics and add debug logging

- Fix save calculation to use original shot velocity before collision
- Add console logging for successful/failed saves
- Enhance goalie save logic with proper difficulty scaling
- Maintain collision physics while preserving save accuracy

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pierre Wessman 2025-09-19 12:34:35 +02:00
parent 9286b28192
commit 7412d917ee
2 changed files with 239 additions and 20 deletions

View File

@ -59,7 +59,7 @@ class Player {
* @param {Array} players - Array of all players on the ice * @param {Array} players - Array of all players on the ice
*/ */
update(deltaTime, gameState, puck, players) { update(deltaTime, gameState, puck, players) {
this.updateMovement(deltaTime); this.updateMovement(deltaTime, gameState);
this.updateAngle(deltaTime); this.updateAngle(deltaTime);
if (this.role !== 'G') { if (this.role !== 'G') {
@ -73,8 +73,9 @@ 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
* @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
*/ */
updateMovement(deltaTime) { updateMovement(deltaTime, gameState) {
const direction = this.targetPosition.subtract(this.position).normalize(); const direction = this.targetPosition.subtract(this.position).normalize();
const distance = this.position.distance(this.targetPosition); const distance = this.position.distance(this.targetPosition);
@ -93,7 +94,7 @@ class Player {
this.position = this.position.add(this.velocity.multiply(deltaTime)); this.position = this.position.add(this.velocity.multiply(deltaTime));
this.keepInBounds(); this.keepInBounds(gameState);
} }
/** /**
@ -914,9 +915,113 @@ class Player {
* Constrains player position to stay within rink boundaries * Constrains player position to stay within rink boundaries
* Uses hardcoded rink dimensions of 1000x600 * Uses hardcoded rink dimensions of 1000x600
*/ */
keepInBounds() { keepInBounds(gameState = null) {
// Basic rink boundary constraints
this.position.x = Math.max(this.radius, Math.min(1000 - this.radius, this.position.x)); 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)); this.position.y = Math.max(this.radius, Math.min(600 - this.radius, this.position.y));
// Goal wall collision constraints
this.checkGoalWallCollisions(gameState);
}
checkGoalWallCollisions(gameState) {
if (!gameState) return; // Skip if no game state provided
const rink = gameState.rink;
const goalY = rink.centerY;
const goalHeight = rink.goalHeight;
const goalDepth = 25;
const goalXOffset = gameState.renderer?.goalXOffset || 80;
// Goal square dimensions (the light red areas)
const leftGoalSquare = {
left: goalXOffset - goalDepth,
right: goalXOffset,
top: goalY - goalHeight,
bottom: goalY + goalHeight
};
const rightGoalSquare = {
left: rink.width - goalXOffset,
right: rink.width - goalXOffset + goalDepth,
top: goalY - goalHeight,
bottom: goalY + goalHeight
};
// Allow goalies to move freely in their own goal square only
if (this.role === 'G') {
// Check if goalie is in their own goal square
const inOwnLeftGoal = this.team === 'home' &&
this.position.x >= leftGoalSquare.left && this.position.x <= leftGoalSquare.right &&
this.position.y >= leftGoalSquare.top && this.position.y <= leftGoalSquare.bottom;
const inOwnRightGoal = this.team === 'away' &&
this.position.x >= rightGoalSquare.left && this.position.x <= rightGoalSquare.right &&
this.position.y >= rightGoalSquare.top && this.position.y <= rightGoalSquare.bottom;
if (inOwnLeftGoal || inOwnRightGoal) {
return; // Goalie can stay in their own goal square
}
// If goalie is not in their own goal square, they still get blocked like any other player
}
// Prevent any player from entering the left goal square
if (this.position.x + this.radius > leftGoalSquare.left &&
this.position.x - this.radius < leftGoalSquare.right &&
this.position.y + this.radius > leftGoalSquare.top &&
this.position.y - this.radius < leftGoalSquare.bottom) {
// Push player out of goal square to the nearest edge
const distToLeft = Math.abs(this.position.x + this.radius - leftGoalSquare.left);
const distToRight = Math.abs(this.position.x - this.radius - leftGoalSquare.right);
const distToTop = Math.abs(this.position.y + this.radius - leftGoalSquare.top);
const distToBottom = Math.abs(this.position.y - this.radius - leftGoalSquare.bottom);
const minDist = Math.min(distToLeft, distToRight, distToTop, distToBottom);
if (minDist === distToLeft) {
this.position.x = leftGoalSquare.left - this.radius;
this.velocity.x = Math.min(0, this.velocity.x);
} else if (minDist === distToRight) {
this.position.x = leftGoalSquare.right + this.radius;
this.velocity.x = Math.max(0, this.velocity.x);
} else if (minDist === distToTop) {
this.position.y = leftGoalSquare.top - this.radius;
this.velocity.y = Math.min(0, this.velocity.y);
} else if (minDist === distToBottom) {
this.position.y = leftGoalSquare.bottom + this.radius;
this.velocity.y = Math.max(0, this.velocity.y);
}
}
// Prevent any player from entering the right goal square
if (this.position.x + this.radius > rightGoalSquare.left &&
this.position.x - this.radius < rightGoalSquare.right &&
this.position.y + this.radius > rightGoalSquare.top &&
this.position.y - this.radius < rightGoalSquare.bottom) {
// Push player out of goal square to the nearest edge
const distToLeft = Math.abs(this.position.x + this.radius - rightGoalSquare.left);
const distToRight = Math.abs(this.position.x - this.radius - rightGoalSquare.right);
const distToTop = Math.abs(this.position.y + this.radius - rightGoalSquare.top);
const distToBottom = Math.abs(this.position.y - this.radius - rightGoalSquare.bottom);
const minDist = Math.min(distToLeft, distToRight, distToTop, distToBottom);
if (minDist === distToLeft) {
this.position.x = rightGoalSquare.left - this.radius;
this.velocity.x = Math.min(0, this.velocity.x);
} else if (minDist === distToRight) {
this.position.x = rightGoalSquare.right + this.radius;
this.velocity.x = Math.max(0, this.velocity.x);
} else if (minDist === distToTop) {
this.position.y = rightGoalSquare.top - this.radius;
this.velocity.y = Math.min(0, this.velocity.y);
} else if (minDist === distToBottom) {
this.position.y = rightGoalSquare.bottom + this.radius;
this.velocity.y = Math.max(0, this.velocity.y);
}
}
} }
/** /**

View File

@ -90,6 +90,10 @@ class Puck {
const rink = gameState.rink; const rink = gameState.rink;
let collision = false; let collision = false;
// Check goal wall collisions first
collision = this.checkGoalWallCollisions(gameState) || collision;
// Check regular board collisions (but skip goal areas)
if (this.position.x - this.radius <= 0 || this.position.x + this.radius >= rink.width) { if (this.position.x - this.radius <= 0 || this.position.x + this.radius >= rink.width) {
if (this.isInGoal(gameState)) { if (this.isInGoal(gameState)) {
this.handleGoal(gameState); this.handleGoal(gameState);
@ -112,6 +116,77 @@ class Puck {
} }
} }
checkGoalWallCollisions(gameState) {
const rink = gameState.rink;
const goalY = rink.centerY;
const goalHeight = rink.goalHeight;
const goalDepth = 25;
const goalXOffset = gameState.renderer?.goalXOffset || 80;
let collision = false;
// Left goal walls
const leftGoalRight = goalXOffset;
const leftGoalLeft = goalXOffset - goalDepth;
const leftGoalTop = goalY - goalHeight;
const leftGoalBottom = goalY + goalHeight;
// Right goal walls
const rightGoalLeft = rink.width - goalXOffset;
const rightGoalRight = rink.width - goalXOffset + goalDepth;
const rightGoalTop = goalY - goalHeight;
const rightGoalBottom = goalY + goalHeight;
// Check left goal back wall collision
if (this.position.x - this.radius <= leftGoalLeft &&
this.position.y >= leftGoalTop && this.position.y <= leftGoalBottom) {
this.velocity.x = Math.abs(this.velocity.x); // Bounce away from wall
this.position.x = leftGoalLeft + this.radius;
collision = true;
}
// Check right goal back wall collision
if (this.position.x + this.radius >= rightGoalRight &&
this.position.y >= rightGoalTop && this.position.y <= rightGoalBottom) {
this.velocity.x = -Math.abs(this.velocity.x); // Bounce away from wall
this.position.x = rightGoalRight - this.radius;
collision = true;
}
// Check left goal side walls
if (this.position.x >= leftGoalLeft && this.position.x <= leftGoalRight) {
// Top side wall
if (this.position.y - this.radius <= leftGoalTop && this.position.y >= leftGoalTop - 20) {
this.velocity.y = Math.abs(this.velocity.y);
this.position.y = leftGoalTop + this.radius;
collision = true;
}
// Bottom side wall
if (this.position.y + this.radius >= leftGoalBottom && this.position.y <= leftGoalBottom + 20) {
this.velocity.y = -Math.abs(this.velocity.y);
this.position.y = leftGoalBottom - this.radius;
collision = true;
}
}
// Check right goal side walls
if (this.position.x >= rightGoalLeft && this.position.x <= rightGoalRight) {
// Top side wall
if (this.position.y - this.radius <= rightGoalTop && this.position.y >= rightGoalTop - 20) {
this.velocity.y = Math.abs(this.velocity.y);
this.position.y = rightGoalTop + this.radius;
collision = true;
}
// Bottom side wall
if (this.position.y + this.radius >= rightGoalBottom && this.position.y <= rightGoalBottom + 20) {
this.velocity.y = -Math.abs(this.velocity.y);
this.position.y = rightGoalBottom - this.radius;
collision = true;
}
}
return collision;
}
isInGoal(gameState) { isInGoal(gameState) {
const goalY = gameState.rink.centerY; const goalY = gameState.rink.centerY;
const goalHeight = gameState.rink.goalHeight; const goalHeight = gameState.rink.goalHeight;
@ -125,16 +200,36 @@ class Puck {
} }
handleGoal(gameState) { handleGoal(gameState) {
const team = this.position.x <= 20 ? 'away' : 'home'; // Determine which goal was scored in and validate the direction
gameState.addGoal(team); let scoringTeam = null;
if (this.position.x <= 20) {
// Puck is in the LEFT goal (home team's goal)
// Only count as goal if puck came from the right side (positive x velocity)
if (this.velocity.x > 0 || (this.lastTeamTouch === 'away')) {
scoringTeam = 'away'; // Away team scored on home team's goal
}
} else if (this.position.x >= gameState.rink.width - 20) {
// Puck is in the RIGHT goal (away team's goal)
// Only count as goal if puck came from the left side (negative x velocity)
if (this.velocity.x < 0 || (this.lastTeamTouch === 'home')) {
scoringTeam = 'home'; // Home team scored on away team's goal
}
}
// Only award goal if it was a valid scoring direction
if (scoringTeam) {
gameState.addGoal(scoringTeam);
gameState.emit('goal-scored', {
team: scoringTeam,
scorer: this.lastPlayerTouch,
position: this.position.copy()
});
}
// Reset puck position regardless of whether goal was valid
this.reset(gameState.rink.centerX, gameState.rink.centerY); this.reset(gameState.rink.centerX, gameState.rink.centerY);
gameState.emit('goal-scored', {
team,
scorer: this.lastPlayerTouch,
position: this.position.copy()
});
} }
checkPlayerCollisions(players, gameState) { checkPlayerCollisions(players, gameState) {
@ -173,15 +268,25 @@ class Puck {
if (speed > 0) return; if (speed > 0) return;
const totalMass = this.mass + player.mass; // Handle goalies differently - check for save BEFORE applying collision physics
const impulse = 2 * speed / totalMass; if (player.role === 'G' && this.velocity.magnitude() > 50) {
// Store original velocity before collision
const originalVelocity = this.velocity.copy();
this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution)); // Apply collision physics first
const totalMass = this.mass + player.mass;
const impulse = 2 * speed / totalMass;
this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution));
// Now check if goalie makes the save
this.handleGoalieSave(player, gameState, originalVelocity);
} else if (player.role !== 'G') {
// Regular player collision - apply physics and pickup puck
const totalMass = this.mass + player.mass;
const impulse = 2 * speed / totalMass;
this.velocity = this.velocity.subtract(direction.multiply(impulse * player.mass * this.restitution));
if (player.role !== 'G') {
this.pickupPuck(player, gameState, players); this.pickupPuck(player, gameState, players);
} else if (player.role === 'G' && this.velocity.magnitude() > 50) {
this.handleGoalieSave(player, gameState);
} }
} }
} }
@ -203,12 +308,16 @@ class Puck {
}); });
} }
handleGoalieSave(goalie, gameState) { handleGoalieSave(goalie, gameState, originalVelocity = null) {
// Use original velocity for save calculation if provided, otherwise current velocity
const velocityForCalculation = originalVelocity || this.velocity;
const saveChance = goalie.attributes.defense / 100; const saveChance = goalie.attributes.defense / 100;
const shotSpeed = this.velocity.magnitude(); const shotSpeed = velocityForCalculation.magnitude();
const difficulty = Math.min(1, shotSpeed / 500); const difficulty = Math.min(1, shotSpeed / 500);
if (Math.random() < saveChance * (1 - difficulty * 0.5)) { if (Math.random() < saveChance * (1 - difficulty * 0.5)) {
console.log("Successful save")
// Successful save - reduce puck velocity significantly
this.velocity = this.velocity.multiply(-0.3); this.velocity = this.velocity.multiply(-0.3);
gameState.addSave(goalie.team); gameState.addSave(goalie.team);
@ -217,6 +326,11 @@ class Puck {
team: goalie.team, team: goalie.team,
difficulty: difficulty difficulty: difficulty
}); });
} else {
console.log("Failed save")
// 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
this.velocity = this.velocity.multiply(0.8); // Slightly reduce speed but let it continue
} }
} }