From 1d257a5757ea728f9fd97d2a192716fb5b7ce1c4 Mon Sep 17 00:00:00 2001 From: Pierre Wessman <4029607+pierrewessman@users.noreply.github.com> Date: Thu, 2 Oct 2025 13:48:27 +0200 Subject: [PATCH] Add realistic tackle mechanics with approach angle and velocity modifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Calculate approach angle between tackler velocity and target direction - Apply angle-based modifiers: head-on (100%) > side (70%) > behind (40%) > opposite (20%) - Calculate closing speed from relative velocity differential - Apply velocity modifiers: weak <1 m/s (0.3x), solid >3 m/s (1.5x) - Combine modifiers: finalSuccess = base × angleModifier × velocityModifier - Enhanced debug logging shows all components of tackle calculation - Head-on charges now much less effective (~15-30% vs previous ~50%) - Rewards proper positioning, blindside hits, and high closing speeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/config/constants.ts | 18 +++++++- src/game/GameScene.ts | 92 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index 22a212f..08129c9 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -56,7 +56,7 @@ export const CENTER_DOT_RADIUS = 5; // pixels // AI/Behavior constants export const SHOOTING_RANGE = 10; // meters - max distance to attempt shot -export const SHOOTING_ANGLE_THRESHOLD = Math.PI / 4; // radians (45 degrees) +export const SHOOTING_ANGLE_THRESHOLD = Math.PI / 3; // radians (60 degrees) export const GOALIE_RANGE = 3; // meters - how far goalie moves from center // Collision avoidance constants @@ -69,3 +69,19 @@ export const TACKLE_PUCK_LOOSE_CHANCE = 0.6; // Chance puck becomes loose export const TACKLE_COOLDOWN = 1000; // ms - time between tackles for same player export const TACKLE_FALL_DURATION = 500; // ms - time player stays down after being tackled export const TACKLE_MIN_SPEED = 2; // m/s - minimum speed required to execute tackle + +// Tackle angle modifiers (based on approach angle) +export const TACKLE_ANGLE_HEAD_ON_THRESHOLD = Math.PI / 4; // 45° - best check angle +export const TACKLE_ANGLE_SIDE_THRESHOLD = Math.PI / 2; // 90° - glancing blow +export const TACKLE_ANGLE_BEHIND_THRESHOLD = (3 * Math.PI) / 4; // 135° - awkward angle + +export const TACKLE_ANGLE_HEAD_ON_MODIFIER = 1.0; // 100% effectiveness +export const TACKLE_ANGLE_SIDE_MODIFIER = 0.7; // 70% effectiveness +export const TACKLE_ANGLE_BEHIND_MODIFIER = 0.4; // 40% effectiveness +export const TACKLE_ANGLE_OPPOSITE_MODIFIER = 0.2; // 20% effectiveness + +// Tackle velocity differential modifiers +export const TACKLE_CLOSING_SPEED_WEAK = 1; // m/s - very weak check +export const TACKLE_CLOSING_SPEED_SOLID = 3; // m/s - solid check +export const TACKLE_VELOCITY_MODIFIER_MIN = 0.3; // Minimum modifier for very low closing speed +export const TACKLE_VELOCITY_MODIFIER_MAX = 1.5; // Maximum modifier for high closing speed diff --git a/src/game/GameScene.ts b/src/game/GameScene.ts index d4b7227..f3db32d 100644 --- a/src/game/GameScene.ts +++ b/src/game/GameScene.ts @@ -18,7 +18,18 @@ import { SHOT_SPEED, TACKLE_SUCCESS_MODIFIER, TACKLE_PUCK_LOOSE_CHANCE, - TACKLE_MIN_SPEED + TACKLE_MIN_SPEED, + TACKLE_ANGLE_HEAD_ON_THRESHOLD, + TACKLE_ANGLE_SIDE_THRESHOLD, + TACKLE_ANGLE_BEHIND_THRESHOLD, + TACKLE_ANGLE_HEAD_ON_MODIFIER, + TACKLE_ANGLE_SIDE_MODIFIER, + TACKLE_ANGLE_BEHIND_MODIFIER, + TACKLE_ANGLE_OPPOSITE_MODIFIER, + TACKLE_CLOSING_SPEED_WEAK, + TACKLE_CLOSING_SPEED_SOLID, + TACKLE_VELOCITY_MODIFIER_MIN, + TACKLE_VELOCITY_MODIFIER_MAX } from '../config/constants'; import { Goal } from './Goal'; import { Puck } from '../entities/Puck'; @@ -326,16 +337,85 @@ export class GameScene extends Phaser.Scene { return; // Tackle cannot occur } - // Calculate tackle success based on tackling vs balance - // Formula: (tackling / (tackling + balance)) * modifier + // 1. Calculate approach angle modifier + // Get velocity vectors (using physics body velocity) + const tacklerVelX = tackler.body.velocity.x / SCALE; // Convert to m/s + const tacklerVelY = tackler.body.velocity.y / SCALE; + const tackledVelX = tackled.body.velocity.x / SCALE; + const tackledVelY = tackled.body.velocity.y / SCALE; + + // Calculate angle of attack: angle between tackler's velocity and direction to tackled player + const dx = tackled.gameX - tackler.gameX; + const dy = tackled.gameY - tackler.gameY; + const angleToTarget = Math.atan2(dy, dx); + const tacklerVelocityAngle = Math.atan2(tacklerVelY, tacklerVelX); + + // Angle difference (how aligned is tackler's movement with target direction) + let approachAngle = Math.abs(angleToTarget - tacklerVelocityAngle); + // Normalize to 0-PI range + if (approachAngle > Math.PI) approachAngle = 2 * Math.PI - approachAngle; + + // Determine angle modifier based on approach angle + let angleModifier: number; + if (approachAngle <= TACKLE_ANGLE_HEAD_ON_THRESHOLD) { + angleModifier = TACKLE_ANGLE_HEAD_ON_MODIFIER; // Head-on + } else if (approachAngle <= TACKLE_ANGLE_SIDE_THRESHOLD) { + angleModifier = TACKLE_ANGLE_SIDE_MODIFIER; // Side angle + } else if (approachAngle <= TACKLE_ANGLE_BEHIND_THRESHOLD) { + angleModifier = TACKLE_ANGLE_BEHIND_MODIFIER; // From behind + } else { + angleModifier = TACKLE_ANGLE_OPPOSITE_MODIFIER; // Opposite direction + } + + // 2. Calculate relative velocity differential (closing speed) + // Project velocities onto the line connecting the players + const distSq = dx * dx + dy * dy; + const dist = Math.sqrt(distSq); + + if (dist < 0.01) { + // Players are on top of each other, skip velocity calculation + return; + } + + const dirX = dx / dist; // Unit vector toward tackled player + const dirY = dy / dist; + + // Closing velocity = tackler's velocity toward target - tackled's velocity toward tackler + const tacklerClosingVel = tacklerVelX * dirX + tacklerVelY * dirY; + const tackledClosingVel = -(tackledVelX * dirX + tackledVelY * dirY); // Negative because measuring toward tackler + const closingSpeed = tacklerClosingVel + tackledClosingVel; + + // Calculate velocity modifier based on closing speed + // Map closing speed to modifier range + let velocityModifier: number; + if (closingSpeed <= TACKLE_CLOSING_SPEED_WEAK) { + velocityModifier = TACKLE_VELOCITY_MODIFIER_MIN; + } else if (closingSpeed >= TACKLE_CLOSING_SPEED_SOLID) { + velocityModifier = TACKLE_VELOCITY_MODIFIER_MAX; + } else { + // Linear interpolation between weak and solid + const ratio = (closingSpeed - TACKLE_CLOSING_SPEED_WEAK) / + (TACKLE_CLOSING_SPEED_SOLID - TACKLE_CLOSING_SPEED_WEAK); + velocityModifier = TACKLE_VELOCITY_MODIFIER_MIN + + ratio * (TACKLE_VELOCITY_MODIFIER_MAX - TACKLE_VELOCITY_MODIFIER_MIN); + } + + // 3. Calculate base tackle success based on tackling vs balance const tacklerSkill = tackler.attributes.tackling; const tackledBalance = tackled.attributes.balance; - const successChance = (tacklerSkill / (tacklerSkill + tackledBalance)) * TACKLE_SUCCESS_MODIFIER; - const success = Math.random() < successChance; + const baseSuccess = (tacklerSkill / (tacklerSkill + tackledBalance)) * TACKLE_SUCCESS_MODIFIER; + + // 4. Apply all modifiers + const finalSuccessChance = baseSuccess * angleModifier * velocityModifier; + const success = Math.random() < finalSuccessChance; console.log( - `[Tackle] ${tackler.id} (tackling: ${tacklerSkill}, speed: ${tacklerSpeed.toFixed(1)} m/s) tackles ${tackled.id} (balance: ${tackledBalance}) - ${success ? 'SUCCESS' : 'FAILED'} (${(successChance * 100).toFixed(1)}%)` + `[Tackle] ${tackler.id} → ${tackled.id} | ` + + `Base: ${(baseSuccess * 100).toFixed(1)}% | ` + + `Angle: ${(approachAngle * 180 / Math.PI).toFixed(0)}° (×${angleModifier.toFixed(2)}) | ` + + `Closing: ${closingSpeed.toFixed(1)} m/s (×${velocityModifier.toFixed(2)}) | ` + + `Final: ${(finalSuccessChance * 100).toFixed(1)}% → ${success ? 'SUCCESS' : 'FAILED'}` ); // Mark tackle performed (cooldown starts)