Add physics-based player detection with field-of-view filtering
Replace distance-based opponent detection with physics sensors and angle filtering for more realistic spatial awareness. Players now detect nearby opponents/teammates through Phaser overlap zones with 180° forward field-of-view. Key changes: - Add detection sensor to Player with physics overlap tracking - Add NEAR_PLAYER_DETECTION_ANGLE constant for FOV filtering - Refactor PuckCarrierBehavior to use physics-based detection - Add debug visualization showing detection arc (wedge shape) - Fix TypeScript unused variable errors for production build 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c749308b49
commit
ca6a444dec
@ -3,7 +3,8 @@
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
"Bash(git push:*)",
|
||||
"Bash(pnpm run:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@ -73,6 +73,7 @@ export const GOALIE_RANGE = 3; // meters - how far goalie
|
||||
|
||||
// Collision avoidance constants
|
||||
export const NEAR_PLAYER_DETECTION_RADIUS = 4; // meters - radius to detect nearby players (threats, teammates, etc.)
|
||||
export const NEAR_PLAYER_DETECTION_ANGLE = Math.PI; // radians (180 degrees) - field of view for detection (half-circle in front)
|
||||
export const EVASION_ANGLE = Math.PI / 3; // radians (60 degrees) - angle to turn when evading
|
||||
|
||||
// Tackle constants
|
||||
|
||||
@ -12,7 +12,8 @@ import {
|
||||
TACKLE_FALL_DURATION,
|
||||
PLAYER_ACCELERATION,
|
||||
PLAYER_DECELERATION,
|
||||
NEAR_PLAYER_DETECTION_RADIUS
|
||||
NEAR_PLAYER_DETECTION_RADIUS,
|
||||
NEAR_PLAYER_DETECTION_ANGLE
|
||||
} from '../config/constants';
|
||||
import { CoordinateUtils } from '../utils/coordinates';
|
||||
import { MathUtils } from '../utils/math';
|
||||
@ -62,6 +63,12 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
// Direction indicator
|
||||
private directionIndicator!: Phaser.GameObjects.Graphics;
|
||||
|
||||
// Nearby player detection (physics-based)
|
||||
public detectionSensor?: Phaser.GameObjects.Zone;
|
||||
public nearbyPlayers: Set<Player> = new Set();
|
||||
public nearbyOpponents: Set<Player> = new Set();
|
||||
public nearbyTeammates: Set<Player> = new Set();
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
id: string,
|
||||
@ -288,6 +295,9 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
this.gameX = gamePos.x;
|
||||
this.gameY = gamePos.y;
|
||||
|
||||
// Update detection sensor position
|
||||
this.updateSensorPosition();
|
||||
|
||||
// Update debug visualizations
|
||||
this.updateDebugVisuals();
|
||||
}
|
||||
@ -325,6 +335,74 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
return this.currentSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create physics-based detection sensor for nearby players
|
||||
* Call this after physics body is set up in GameScene.addPlayer()
|
||||
*/
|
||||
public createDetectionSensor() {
|
||||
// Create invisible zone for physics detection (no visual representation)
|
||||
const sensorRadius = NEAR_PLAYER_DETECTION_RADIUS * SCALE;
|
||||
const sensorDiameter = sensorRadius * 2;
|
||||
this.detectionSensor = this.scene.add.zone(0, 0, sensorDiameter, sensorDiameter);
|
||||
|
||||
// Add physics to sensor
|
||||
this.scene.physics.add.existing(this.detectionSensor);
|
||||
const sensorBody = this.detectionSensor.body as Phaser.Physics.Arcade.Body;
|
||||
sensorBody.setCircle(sensorRadius);
|
||||
|
||||
// Make it a sensor (no collision, only overlap detection)
|
||||
sensorBody.enable = true;
|
||||
sensorBody.setAllowGravity(false);
|
||||
|
||||
// Disable debug rendering for this physics body
|
||||
sensorBody.debugShowBody = false;
|
||||
|
||||
// Position sensor at player location
|
||||
this.updateSensorPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sensor position to match player position
|
||||
* Called every frame in update()
|
||||
*/
|
||||
private updateSensorPosition() {
|
||||
if (!this.detectionSensor) return;
|
||||
|
||||
const screenPos = CoordinateUtils.gameToScreen(this.scene, this.gameX, this.gameY);
|
||||
this.detectionSensor.setPosition(screenPos.x, screenPos.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is within detection field of view (angle filtering)
|
||||
* Returns true if player is within the detection arc in front
|
||||
*/
|
||||
public isPlayerInDetectionRange(other: Player): boolean {
|
||||
if (!this.nearbyPlayers.has(other)) return false;
|
||||
|
||||
// Calculate angle from this player to the other player
|
||||
const angleToOther = Math.atan2(other.gameY - this.gameY, other.gameX - this.gameX);
|
||||
|
||||
// Calculate angle difference from current facing direction
|
||||
const angleDiff = MathUtils.angleDifference(this.currentAngle, angleToOther);
|
||||
|
||||
// Check if within field of view (e.g., 180° = Math.PI for half-circle in front)
|
||||
return Math.abs(angleDiff) <= NEAR_PLAYER_DETECTION_ANGLE / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all opponents within detection range (filtered by angle)
|
||||
*/
|
||||
public getDetectedOpponents(): Player[] {
|
||||
return Array.from(this.nearbyOpponents).filter(p => this.isPlayerInDetectionRange(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all teammates within detection range (filtered by angle)
|
||||
*/
|
||||
public getDetectedTeammates(): Player[] {
|
||||
return Array.from(this.nearbyTeammates).filter(p => this.isPlayerInDetectionRange(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update debug visualizations (target position, path line, and detection circle)
|
||||
*/
|
||||
@ -361,8 +439,31 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
targetScreen.y + markerSize
|
||||
);
|
||||
|
||||
// Draw near player detection circle (always visible in DEBUG mode)
|
||||
this.debugDetectionCircle.lineStyle(2, 0xff00ff, 0.3); // Magenta circle with lower opacity
|
||||
this.debugDetectionCircle.strokeCircle(playerScreen.x, playerScreen.y, NEAR_PLAYER_DETECTION_RADIUS * SCALE);
|
||||
// Draw near player detection arc (field of view as a filled wedge)
|
||||
const halfFOV = NEAR_PLAYER_DETECTION_ANGLE / 2;
|
||||
|
||||
// Convert game angle to screen angle (negate because screen Y is inverted)
|
||||
const screenAngle = -this.currentAngle;
|
||||
const startAngle = screenAngle - halfFOV;
|
||||
const endAngle = screenAngle + halfFOV;
|
||||
|
||||
// Draw filled wedge showing field of view
|
||||
this.debugDetectionCircle.fillStyle(0xff00ff, 0.1); // Magenta with low alpha
|
||||
this.debugDetectionCircle.lineStyle(2, 0xff00ff, 0.4); // Magenta border
|
||||
|
||||
this.debugDetectionCircle.beginPath();
|
||||
this.debugDetectionCircle.moveTo(playerScreen.x, playerScreen.y); // Start at player center
|
||||
this.debugDetectionCircle.arc(
|
||||
playerScreen.x,
|
||||
playerScreen.y,
|
||||
NEAR_PLAYER_DETECTION_RADIUS * SCALE,
|
||||
startAngle,
|
||||
endAngle,
|
||||
false
|
||||
);
|
||||
this.debugDetectionCircle.lineTo(playerScreen.x, playerScreen.y); // Close the wedge
|
||||
this.debugDetectionCircle.closePath();
|
||||
this.debugDetectionCircle.fillPath();
|
||||
this.debugDetectionCircle.strokePath();
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,10 +132,61 @@ export class GameScene extends Phaser.Scene {
|
||||
this.playerGroup.add(player);
|
||||
this.behaviorTrees.set(player.id, new BehaviorTree(player));
|
||||
|
||||
// Create detection sensor for this player
|
||||
player.createDetectionSensor();
|
||||
|
||||
// Setup detection sensor overlaps with other players
|
||||
this.setupPlayerDetection(player);
|
||||
|
||||
// Add collision with all goal posts
|
||||
this.physics.add.collider(player, this.goalPostsGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup physics-based detection sensor overlaps for a player
|
||||
*/
|
||||
setupPlayerDetection(player: Player) {
|
||||
if (!player.detectionSensor) return;
|
||||
|
||||
// Setup overlap detection with all other players
|
||||
this.physics.add.overlap(
|
||||
player.detectionSensor,
|
||||
this.playerGroup,
|
||||
(_sensor, otherPlayer) => {
|
||||
const other = otherPlayer as unknown as Player;
|
||||
|
||||
// Don't detect self
|
||||
if (other.id === player.id) return;
|
||||
|
||||
// Add to nearby players set
|
||||
player.nearbyPlayers.add(other);
|
||||
|
||||
// Categorize by team
|
||||
if (other.team === player.team) {
|
||||
player.nearbyTeammates.add(other);
|
||||
} else {
|
||||
player.nearbyOpponents.add(other);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Important: Clear nearby players when they leave the sensor
|
||||
// We need to check every frame and remove players no longer overlapping
|
||||
this.events.on('update', () => {
|
||||
if (!player.detectionSensor) return;
|
||||
|
||||
// Check each nearby player to see if still overlapping
|
||||
player.nearbyPlayers.forEach(other => {
|
||||
const overlapping = this.physics.overlap(player.detectionSensor!, other);
|
||||
if (!overlapping) {
|
||||
player.nearbyPlayers.delete(other);
|
||||
player.nearbyOpponents.delete(other);
|
||||
player.nearbyTeammates.delete(other);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a player from the scene (can be called at any time for debugging)
|
||||
*/
|
||||
@ -165,7 +216,7 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
private createPuck() {
|
||||
// Initialize puck at center ice (0, 0 in game coordinates)
|
||||
this.puck = new Puck(this, 5, 0);
|
||||
this.puck = new Puck(this, 5, 5);
|
||||
|
||||
// Add collisions between puck and all goal posts with custom bounce handler
|
||||
this.physics.add.collider(this.puck, this.goalPostsGroup, this.handlePuckPostBounce, undefined, this);
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
SHOOTING_ANGLE_THRESHOLD,
|
||||
CLOSE_RANGE_DISTANCE,
|
||||
CLOSE_RANGE_ANGLE_THRESHOLD,
|
||||
NEAR_PLAYER_DETECTION_RADIUS,
|
||||
EVASION_ANGLE,
|
||||
DEBUG
|
||||
} from '../../../config/constants';
|
||||
@ -92,18 +91,19 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect opponents blocking the path to goal
|
||||
* Detect opponents blocking the path to goal using physics-based detection
|
||||
* Returns evasion target if threat detected, null otherwise
|
||||
*
|
||||
* The player will maintain evasion as long as an opponent is:
|
||||
* 1. Within the near player detection radius (close proximity)
|
||||
* 1. Within the near player detection zone (physics sensor)
|
||||
* 2. Within the field of view (angle-based filtering)
|
||||
*
|
||||
* Once an evasion direction is chosen, it persists until the opponent
|
||||
* leaves the threat zone to prevent direction flipping.
|
||||
*/
|
||||
private detectImmediateThreat(
|
||||
player: Player,
|
||||
gameState: GameState,
|
||||
_gameState: GameState,
|
||||
opponentGoalX: number
|
||||
): { approachAngle: number; evasionTargetX: number; evasionTargetY: number; evasionAngle: number } | null {
|
||||
// Get or create debug graphics for this player
|
||||
@ -126,7 +126,7 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
||||
const goalY = 0;
|
||||
const toGoalAngle = Math.atan2(goalY - player.gameY, opponentGoalX - player.gameX);
|
||||
|
||||
// Draw debug line to goal (circle is now rendered by Player class)
|
||||
// Draw debug line to goal (arc is now rendered by Player class)
|
||||
if (DEBUG && debugGraphics) {
|
||||
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
||||
const goalScreen = CoordinateUtils.gameToScreen(player.scene, opponentGoalX, goalY);
|
||||
@ -134,17 +134,14 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
||||
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, goalScreen.x, goalScreen.y);
|
||||
}
|
||||
|
||||
// Find all opponents
|
||||
const opponents = gameState.allPlayers.filter((p: Player) => p.team !== player.team);
|
||||
// Get detected opponents (physics-based with angle filtering)
|
||||
const detectedOpponents = player.getDetectedOpponents();
|
||||
|
||||
// Check if we have any active evasions that are still valid
|
||||
for (const [opponentId, evasion] of playerEvasions.entries()) {
|
||||
const opponent = opponents.find(opp => opp.id === opponentId);
|
||||
const opponent = detectedOpponents.find(opp => opp.id === opponentId);
|
||||
if (opponent) {
|
||||
const distanceToOpponent = MathUtils.distance(player.gameX, player.gameY, opponent.gameX, opponent.gameY);
|
||||
|
||||
// If opponent still in threat zone, maintain the evasion
|
||||
if (distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
||||
// Opponent still detected in threat zone, maintain the evasion
|
||||
const evasionAngle = evasion.side === 'left' ? toGoalAngle + EVASION_ANGLE : toGoalAngle - EVASION_ANGLE;
|
||||
const evasionDistance = 20;
|
||||
const evasionTargetX = player.gameX + Math.cos(evasionAngle) * evasionDistance;
|
||||
@ -184,25 +181,17 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
||||
// Opponent left threat zone, clear the evasion
|
||||
playerEvasions.delete(opponentId);
|
||||
}
|
||||
} else {
|
||||
// Opponent no longer exists, clear the evasion
|
||||
playerEvasions.delete(opponentId);
|
||||
}
|
||||
}
|
||||
|
||||
// No active evasions, check for new threats
|
||||
for (const opponent of opponents) {
|
||||
const distanceToOpponent = MathUtils.distance(player.gameX, player.gameY, opponent.gameX, opponent.gameY);
|
||||
|
||||
// Debug: Draw all opponents in threat zone
|
||||
if (DEBUG && debugGraphics && distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
||||
for (const opponent of detectedOpponents) {
|
||||
// Debug: Draw all detected opponents
|
||||
if (DEBUG && debugGraphics) {
|
||||
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
||||
debugGraphics.lineStyle(2, 0xffa500, 0.6); // Orange circle
|
||||
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 12);
|
||||
}
|
||||
|
||||
// Check if opponent is within threat zone
|
||||
if (distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
||||
// Calculate angle from player to opponent
|
||||
const angleToOpponent = Math.atan2(
|
||||
opponent.gameY - player.gameY,
|
||||
@ -260,7 +249,6 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user