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": {
|
||||
"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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n)\")"
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"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 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
|
||||
|
||||
@ -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<string, BehaviorTree> = new Map();
|
||||
|
||||
// Track last reception attempt time for each player to avoid spamming attempts
|
||||
private lastReceptionAttempt: Map<string, number> = 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) {
|
||||
// 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(`${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
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user