Implement skill-based puck reception with speed penalty mechanics

- Add handling attribute to PlayerAttributes (0-100 skill rating)
- Add puck reception constants: base chance, speed thresholds, check interval
- Replace instant pickup with skill-based reception probability calculation
- Reception chance factors: handling skill and puck speed
- Slow pucks (< 5 m/s) easy to receive, fast pucks (> 20 m/s) very difficult
- Add reception cooldown (100ms) to prevent spammy attempts
- Add comprehensive logging for reception success/failure with stats
- Adjust shooting logic: check shots before evasion, prevent behind-goal shots
- Add close-range shooting (≤3m) with wider angle threshold (120° vs 60°)
- Update Claude settings to simplified permission patterns

🤖 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 14:25:40 +02:00
parent f342688143
commit 2e2f065838
5 changed files with 124 additions and 20 deletions

View File

@ -1,11 +1,9 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git push:*)",
"Bash(pnpm run:*)",
"Bash(git add:*)", "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 <noreply@anthropic.com>\nEOF\n)\")", "Bash(git commit:*)",
"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 <noreply@anthropic.com>\nEOF\n)\")" "Bash(git push:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -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 MAX_PUCK_VELOCITY = 50; // m/s
export const SHOT_SPEED = 30; // 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 // Goal constants
export const GOAL_POST_THICKNESS = 0.3; // meters export const GOAL_POST_THICKNESS = 0.3; // meters
export const GOAL_BAR_THICKNESS = 0.4; // meters export const GOAL_BAR_THICKNESS = 0.4; // meters

View File

@ -31,7 +31,11 @@ import {
TACKLE_VELOCITY_MODIFIER_MIN, TACKLE_VELOCITY_MODIFIER_MIN,
TACKLE_VELOCITY_MODIFIER_MAX, TACKLE_VELOCITY_MODIFIER_MAX,
POST_BOUNCE_SPEED_REDUCTION, 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'; } from '../config/constants';
import { Goal } from './Goal'; import { Goal } from './Goal';
import { Puck } from '../entities/Puck'; import { Puck } from '../entities/Puck';
@ -46,6 +50,9 @@ export class GameScene extends Phaser.Scene {
private players: Player[] = []; private players: Player[] = [];
private behaviorTrees: Map<string, BehaviorTree> = new Map(); private behaviorTrees: Map<string, BehaviorTree> = new Map();
// Track last reception attempt time for each player to avoid spamming attempts
private lastReceptionAttempt: Map<string, number> = new Map();
constructor() { constructor() {
super({ key: 'GameScene' }); super({ key: 'GameScene' });
} }
@ -67,7 +74,7 @@ export class GameScene extends Phaser.Scene {
'C', 'C',
-1, -1,
0, 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 // Create one away defender at (15, 0) - right side for defending
@ -78,7 +85,7 @@ export class GameScene extends Phaser.Scene {
'LD', 'LD',
-15, -15,
0, 0,
{ speed: 80, skill: 70, tackling: 85, balance: 80 } { speed: 80, skill: 70, tackling: 85, balance: 80, handling: 65 }
); );
this.players.push(homeCenter, awayDefender); this.players.push(homeCenter, awayDefender);
@ -248,20 +255,91 @@ export class GameScene extends Phaser.Scene {
this.rightGoal.checkGoal(this.puck); this.rightGoal.checkGoal(this.puck);
} }
/**
* Check if players can receive the puck (skill-based with puck speed factor)
*/
private checkPuckPickup() { private checkPuckPickup() {
if (this.puck.state !== 'loose') return; 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 // Check each player's distance to puck
this.players.forEach(player => { this.players.forEach(player => {
const distance = MathUtils.distance(player.gameX, player.gameY, this.puck.gameX, this.puck.gameY); const distance = MathUtils.distance(player.gameX, player.gameY, this.puck.gameX, this.puck.gameY);
if (distance < PUCK_PICKUP_RADIUS) { if (distance < PUCK_PICKUP_RADIUS) {
// 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); this.puck.setCarrier(player.id, player.team);
console.log(`${player.id} picked up the puck`); 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 * Handle puck bouncing off goal posts with randomized angle and speed reduction
*/ */

View File

@ -5,6 +5,8 @@ import {
GOAL_LINE_OFFSET, GOAL_LINE_OFFSET,
SHOOTING_RANGE, SHOOTING_RANGE,
SHOOTING_ANGLE_THRESHOLD, SHOOTING_ANGLE_THRESHOLD,
CLOSE_RANGE_DISTANCE,
CLOSE_RANGE_ANGLE_THRESHOLD,
THREAT_DETECTION_RADIUS, THREAT_DETECTION_RADIUS,
EVASION_ANGLE, EVASION_ANGLE,
DEBUG, DEBUG,
@ -62,16 +64,6 @@ export class PuckCarrierBehavior extends BehaviorNode {
private decidePuckCarrierAction(player: Player, gameState: GameState): PlayerAction { private decidePuckCarrierAction(player: Player, gameState: GameState): PlayerAction {
const opponentGoalX = player.team === 'home' ? GOAL_LINE_OFFSET : -GOAL_LINE_OFFSET; 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 // Check for shooting opportunity
if (this.hasGoodShot(player, opponentGoalX)) { if (this.hasGoodShot(player, opponentGoalX)) {
console.log(`${player.id} is shooting!`); 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 // Default: Carry puck toward opponent's net
return { return {
type: 'skate_with_puck', type: 'skate_with_puck',
@ -270,10 +272,24 @@ export class PuckCarrierBehavior extends BehaviorNode {
/** /**
* Evaluate if player has a good shooting opportunity * 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 { private hasGoodShot(player: Player, opponentGoalX: number): boolean {
const goalY = 0; 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 // Calculate distance to goal
const distance = MathUtils.distance(player.gameX, player.gameY, opponentGoalX, goalY); const distance = MathUtils.distance(player.gameX, player.gameY, opponentGoalX, goalY);
@ -299,7 +315,12 @@ export class PuckCarrierBehavior extends BehaviorNode {
angleDiff = 2 * Math.PI - angleDiff; 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; return angleDiff < SHOOTING_ANGLE_THRESHOLD;
} }
} }

View File

@ -29,6 +29,7 @@ export interface PlayerAttributes {
skill: number; // 0-100: pass/shot accuracy, decision quality skill: number; // 0-100: pass/shot accuracy, decision quality
tackling: number; // 0-100: ability to execute successful tackles tackling: number; // 0-100: ability to execute successful tackles
balance: number; // 0-100: ability to resist being tackled balance: number; // 0-100: ability to resist being tackled
handling: number; // 0-100: ability to receive and control the puck
} }
/** /**