diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e3ae688..a1a7ea3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,11 +1,9 @@ { "permissions": { "allow": [ - "Bash(git push:*)", - "Bash(pnpm run:*)", "Bash(git add:*)", - "Bash(git commit -m \"$(cat <<''EOF''\nImplement collision avoidance for puck carrier with debug visualization\n\n- Add threat detection system that detects opponents within 3m radius\n- Puck carrier automatically evades when opponent is in path to goal\n- Evasion direction (left/right) persists while opponent in threat zone\n- Fix behavior tree instantiation to reuse trees per player\n- Add comprehensive debug visualization (magenta circle, threat indicators)\n- Fix DefensiveBehavior type errors (carrierTeam -> possession)\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", - "Bash(git commit -m \"$(cat <<''EOF''\nAdd minimum speed requirement for tackle execution\n\n- Players must be moving at least 2 m/s to execute a tackle\n- Add TACKLE_MIN_SPEED constant (2 m/s) in constants.ts\n- Add Player.getCurrentSpeed() method to expose current speed\n- Check tackler speed in executeTackle() before attempting tackle\n- Log speed in tackle debug output for tuning\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")" + "Bash(git commit:*)", + "Bash(git push:*)" ], "deny": [], "ask": [] diff --git a/src/config/constants.ts b/src/config/constants.ts index f12bd03..b8d3361 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -46,6 +46,12 @@ export const PUCK_CARRY_DISTANCE = 1.0; // meters in front of player export const MAX_PUCK_VELOCITY = 50; // m/s export const SHOT_SPEED = 30; // m/s +// Puck reception constants +export const PUCK_RECEPTION_BASE_CHANCE = 0.8; // Base chance to receive puck (modified by handling skill) +export const PUCK_RECEPTION_SPEED_EASY = 5; // m/s - easy to receive below this speed +export const PUCK_RECEPTION_SPEED_HARD = 20; // m/s - very difficult to receive above this speed +export const PUCK_RECEPTION_CHECK_INTERVAL = 100; // ms - how often to check for reception (avoids spammy attempts) + // Goal constants export const GOAL_POST_THICKNESS = 0.3; // meters export const GOAL_BAR_THICKNESS = 0.4; // meters diff --git a/src/game/GameScene.ts b/src/game/GameScene.ts index fe47020..e138b08 100644 --- a/src/game/GameScene.ts +++ b/src/game/GameScene.ts @@ -31,7 +31,11 @@ import { TACKLE_VELOCITY_MODIFIER_MIN, TACKLE_VELOCITY_MODIFIER_MAX, POST_BOUNCE_SPEED_REDUCTION, - POST_BOUNCE_ANGLE_RANDOMNESS + POST_BOUNCE_ANGLE_RANDOMNESS, + PUCK_RECEPTION_BASE_CHANCE, + PUCK_RECEPTION_SPEED_EASY, + PUCK_RECEPTION_SPEED_HARD, + PUCK_RECEPTION_CHECK_INTERVAL } from '../config/constants'; import { Goal } from './Goal'; import { Puck } from '../entities/Puck'; @@ -46,6 +50,9 @@ export class GameScene extends Phaser.Scene { private players: Player[] = []; private behaviorTrees: Map = new Map(); + // Track last reception attempt time for each player to avoid spamming attempts + private lastReceptionAttempt: Map = new Map(); + constructor() { super({ key: 'GameScene' }); } @@ -67,7 +74,7 @@ export class GameScene extends Phaser.Scene { 'C', -1, 0, - { speed: 70, skill: 75, tackling: 70, balance: 75 } + { speed: 70, skill: 75, tackling: 70, balance: 75, handling: 80 } ); // Create one away defender at (15, 0) - right side for defending @@ -78,7 +85,7 @@ export class GameScene extends Phaser.Scene { 'LD', -15, 0, - { speed: 80, skill: 70, tackling: 85, balance: 80 } + { speed: 80, skill: 70, tackling: 85, balance: 80, handling: 65 } ); this.players.push(homeCenter, awayDefender); @@ -248,20 +255,91 @@ export class GameScene extends Phaser.Scene { this.rightGoal.checkGoal(this.puck); } + /** + * Check if players can receive the puck (skill-based with puck speed factor) + */ private checkPuckPickup() { if (this.puck.state !== 'loose') return; + const currentTime = Date.now(); + + // Calculate puck speed in m/s + const puckVelX = this.puck.body.velocity.x / SCALE; + const puckVelY = this.puck.body.velocity.y / SCALE; + const puckSpeed = Math.sqrt(puckVelX * puckVelX + puckVelY * puckVelY); + // Check each player's distance to puck this.players.forEach(player => { const distance = MathUtils.distance(player.gameX, player.gameY, this.puck.gameX, this.puck.gameY); if (distance < PUCK_PICKUP_RADIUS) { - this.puck.setCarrier(player.id, player.team); - console.log(`${player.id} picked up the puck`); + // Check if enough time has passed since last attempt (prevents spam) + const lastAttempt = this.lastReceptionAttempt.get(player.id) || 0; + if (currentTime - lastAttempt < PUCK_RECEPTION_CHECK_INTERVAL) { + return; // Too soon since last attempt + } + + // Update last attempt time + this.lastReceptionAttempt.set(player.id, currentTime); + + // Calculate reception success chance based on handling skill and puck speed + const receptionChance = this.calculateReceptionChance(player.attributes.handling, puckSpeed); + + // Attempt to receive the puck + if (Math.random() < receptionChance) { + this.puck.setCarrier(player.id, player.team); + console.log( + `[Reception] ${player.id} received puck | ` + + `Handling: ${player.attributes.handling} | ` + + `Puck speed: ${puckSpeed.toFixed(1)} m/s | ` + + `Success chance: ${(receptionChance * 100).toFixed(1)}%` + ); + } else { + console.log( + `[Reception] ${player.id} FAILED to receive puck | ` + + `Handling: ${player.attributes.handling} | ` + + `Puck speed: ${puckSpeed.toFixed(1)} m/s | ` + + `Success chance: ${(receptionChance * 100).toFixed(1)}%` + ); + } } }); } + /** + * Calculate puck reception success chance based on handling skill and puck speed + * @param handling - Player's handling skill (0-100) + * @param puckSpeed - Puck speed in m/s + * @returns Success chance (0-1) + */ + private calculateReceptionChance(handling: number, puckSpeed: number): number { + // 1. Base chance from handling skill (handling / 100) + // Scale it with the base chance constant + const skillFactor = (handling / 100) * PUCK_RECEPTION_BASE_CHANCE; + + // 2. Speed modifier (easier for slow pucks, harder for fast ones) + let speedModifier: number; + + if (puckSpeed <= PUCK_RECEPTION_SPEED_EASY) { + // Easy reception - no penalty + speedModifier = 1.0; + } else if (puckSpeed >= PUCK_RECEPTION_SPEED_HARD) { + // Very hard reception - 30% of normal chance + speedModifier = 0.3; + } else { + // Linear interpolation between easy and hard thresholds + const speedRange = PUCK_RECEPTION_SPEED_HARD - PUCK_RECEPTION_SPEED_EASY; + const speedExcess = puckSpeed - PUCK_RECEPTION_SPEED_EASY; + const speedRatio = speedExcess / speedRange; + + // Interpolate from 1.0 (easy) to 0.3 (hard) + speedModifier = 1.0 - (speedRatio * 0.7); + } + + // 3. Final reception chance + return skillFactor * speedModifier; + } + /** * Handle puck bouncing off goal posts with randomized angle and speed reduction */ diff --git a/src/systems/behaviors/offensive/PuckCarrierBehavior.ts b/src/systems/behaviors/offensive/PuckCarrierBehavior.ts index db14e0d..df3dc46 100644 --- a/src/systems/behaviors/offensive/PuckCarrierBehavior.ts +++ b/src/systems/behaviors/offensive/PuckCarrierBehavior.ts @@ -5,6 +5,8 @@ import { GOAL_LINE_OFFSET, SHOOTING_RANGE, SHOOTING_ANGLE_THRESHOLD, + CLOSE_RANGE_DISTANCE, + CLOSE_RANGE_ANGLE_THRESHOLD, THREAT_DETECTION_RADIUS, EVASION_ANGLE, DEBUG, @@ -62,16 +64,6 @@ export class PuckCarrierBehavior extends BehaviorNode { private decidePuckCarrierAction(player: Player, gameState: GameState): PlayerAction { const opponentGoalX = player.team === 'home' ? GOAL_LINE_OFFSET : -GOAL_LINE_OFFSET; - // Check for immediate threats and evade if necessary - const threatInfo = this.detectImmediateThreat(player, gameState, opponentGoalX); - if (threatInfo) { - return { - type: 'skate_with_puck', - targetX: threatInfo.evasionTargetX, - targetY: threatInfo.evasionTargetY - }; - } - // Check for shooting opportunity if (this.hasGoodShot(player, opponentGoalX)) { console.log(`${player.id} is shooting!`); @@ -82,6 +74,16 @@ export class PuckCarrierBehavior extends BehaviorNode { }; } + // Check for immediate threats and evade if necessary + const threatInfo = this.detectImmediateThreat(player, gameState, opponentGoalX); + if (threatInfo) { + return { + type: 'skate_with_puck', + targetX: threatInfo.evasionTargetX, + targetY: threatInfo.evasionTargetY + }; + } + // Default: Carry puck toward opponent's net return { type: 'skate_with_puck', @@ -270,10 +272,24 @@ export class PuckCarrierBehavior extends BehaviorNode { /** * Evaluate if player has a good shooting opportunity + * + * Close-range shots (within 3m): Allow wider angles (120°) for backhands/angled shots + * Normal shots (3-10m): Require tighter angles (60°) */ private hasGoodShot(player: Player, opponentGoalX: number): boolean { const goalY = 0; + // Check if player is on the correct side of the goal (not behind it) + // For home team attacking right (opponentGoalX = +26), player must be to the left (player.gameX < opponentGoalX) + // For away team attacking left (opponentGoalX = -26), player must be to the right (player.gameX > opponentGoalX) + const isInFrontOfGoal = player.team === 'home' + ? player.gameX < opponentGoalX - 1 + : player.gameX > opponentGoalX + 1; + + if (!isInFrontOfGoal) { + return false; + } + // Calculate distance to goal const distance = MathUtils.distance(player.gameX, player.gameY, opponentGoalX, goalY); @@ -299,7 +315,12 @@ export class PuckCarrierBehavior extends BehaviorNode { angleDiff = 2 * Math.PI - angleDiff; } - // Good angle if within threshold + // Close-range shots: Allow much wider angles (backhands, sharp-angle shots) + if (distance <= CLOSE_RANGE_DISTANCE) { + return angleDiff < CLOSE_RANGE_ANGLE_THRESHOLD; + } + + // Normal range: Require tighter shooting angle return angleDiff < SHOOTING_ANGLE_THRESHOLD; } } diff --git a/src/types/game.ts b/src/types/game.ts index c952dbf..ebc37c6 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -29,6 +29,7 @@ export interface PlayerAttributes { skill: number; // 0-100: pass/shot accuracy, decision quality tackling: number; // 0-100: ability to execute successful tackles balance: number; // 0-100: ability to resist being tackled + handling: number; // 0-100: ability to receive and control the puck } /**