Add realistic tackle mechanics with approach angle and velocity modifiers

- 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 <noreply@anthropic.com>
This commit is contained in:
Pierre Wessman 2025-10-02 13:48:27 +02:00
parent 3a48483973
commit 1d257a5757
2 changed files with 103 additions and 7 deletions

View File

@ -56,7 +56,7 @@ export const CENTER_DOT_RADIUS = 5; // pixels
// AI/Behavior constants // AI/Behavior constants
export const SHOOTING_RANGE = 10; // meters - max distance to attempt shot 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 export const GOALIE_RANGE = 3; // meters - how far goalie moves from center
// Collision avoidance constants // 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_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_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 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

View File

@ -18,7 +18,18 @@ import {
SHOT_SPEED, SHOT_SPEED,
TACKLE_SUCCESS_MODIFIER, TACKLE_SUCCESS_MODIFIER,
TACKLE_PUCK_LOOSE_CHANCE, 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'; } from '../config/constants';
import { Goal } from './Goal'; import { Goal } from './Goal';
import { Puck } from '../entities/Puck'; import { Puck } from '../entities/Puck';
@ -326,16 +337,85 @@ export class GameScene extends Phaser.Scene {
return; // Tackle cannot occur return; // Tackle cannot occur
} }
// Calculate tackle success based on tackling vs balance // 1. Calculate approach angle modifier
// Formula: (tackling / (tackling + balance)) * 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 tacklerSkill = tackler.attributes.tackling;
const tackledBalance = tackled.attributes.balance; const tackledBalance = tackled.attributes.balance;
const successChance = (tacklerSkill / (tacklerSkill + tackledBalance)) * TACKLE_SUCCESS_MODIFIER; const baseSuccess = (tacklerSkill / (tacklerSkill + tackledBalance)) * TACKLE_SUCCESS_MODIFIER;
const success = Math.random() < successChance;
// 4. Apply all modifiers
const finalSuccessChance = baseSuccess * angleModifier * velocityModifier;
const success = Math.random() < finalSuccessChance;
console.log( 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) // Mark tackle performed (cooldown starts)