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:
parent
f342688143
commit
2e2f065838
@ -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": []
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user