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": [
|
"allow": [
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)",
|
"Bash(git commit:*)",
|
||||||
"Bash(git push:*)"
|
"Bash(git push:*)",
|
||||||
|
"Bash(pnpm run:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export const GOALIE_RANGE = 3; // meters - how far goalie
|
|||||||
|
|
||||||
// Collision avoidance constants
|
// Collision avoidance constants
|
||||||
export const NEAR_PLAYER_DETECTION_RADIUS = 4; // meters - radius to detect nearby players (threats, teammates, etc.)
|
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
|
export const EVASION_ANGLE = Math.PI / 3; // radians (60 degrees) - angle to turn when evading
|
||||||
|
|
||||||
// Tackle constants
|
// Tackle constants
|
||||||
|
|||||||
@ -12,7 +12,8 @@ import {
|
|||||||
TACKLE_FALL_DURATION,
|
TACKLE_FALL_DURATION,
|
||||||
PLAYER_ACCELERATION,
|
PLAYER_ACCELERATION,
|
||||||
PLAYER_DECELERATION,
|
PLAYER_DECELERATION,
|
||||||
NEAR_PLAYER_DETECTION_RADIUS
|
NEAR_PLAYER_DETECTION_RADIUS,
|
||||||
|
NEAR_PLAYER_DETECTION_ANGLE
|
||||||
} from '../config/constants';
|
} from '../config/constants';
|
||||||
import { CoordinateUtils } from '../utils/coordinates';
|
import { CoordinateUtils } from '../utils/coordinates';
|
||||||
import { MathUtils } from '../utils/math';
|
import { MathUtils } from '../utils/math';
|
||||||
@ -62,6 +63,12 @@ export class Player extends Phaser.GameObjects.Container {
|
|||||||
// Direction indicator
|
// Direction indicator
|
||||||
private directionIndicator!: Phaser.GameObjects.Graphics;
|
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(
|
constructor(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
id: string,
|
id: string,
|
||||||
@ -288,6 +295,9 @@ export class Player extends Phaser.GameObjects.Container {
|
|||||||
this.gameX = gamePos.x;
|
this.gameX = gamePos.x;
|
||||||
this.gameY = gamePos.y;
|
this.gameY = gamePos.y;
|
||||||
|
|
||||||
|
// Update detection sensor position
|
||||||
|
this.updateSensorPosition();
|
||||||
|
|
||||||
// Update debug visualizations
|
// Update debug visualizations
|
||||||
this.updateDebugVisuals();
|
this.updateDebugVisuals();
|
||||||
}
|
}
|
||||||
@ -325,6 +335,74 @@ export class Player extends Phaser.GameObjects.Container {
|
|||||||
return this.currentSpeed;
|
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)
|
* Update debug visualizations (target position, path line, and detection circle)
|
||||||
*/
|
*/
|
||||||
@ -361,8 +439,31 @@ export class Player extends Phaser.GameObjects.Container {
|
|||||||
targetScreen.y + markerSize
|
targetScreen.y + markerSize
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw near player detection circle (always visible in DEBUG mode)
|
// Draw near player detection arc (field of view as a filled wedge)
|
||||||
this.debugDetectionCircle.lineStyle(2, 0xff00ff, 0.3); // Magenta circle with lower opacity
|
const halfFOV = NEAR_PLAYER_DETECTION_ANGLE / 2;
|
||||||
this.debugDetectionCircle.strokeCircle(playerScreen.x, playerScreen.y, NEAR_PLAYER_DETECTION_RADIUS * SCALE);
|
|
||||||
|
// 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.playerGroup.add(player);
|
||||||
this.behaviorTrees.set(player.id, new BehaviorTree(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
|
// Add collision with all goal posts
|
||||||
this.physics.add.collider(player, this.goalPostsGroup);
|
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)
|
* 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() {
|
private createPuck() {
|
||||||
// Initialize puck at center ice (0, 0 in game coordinates)
|
// 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
|
// Add collisions between puck and all goal posts with custom bounce handler
|
||||||
this.physics.add.collider(this.puck, this.goalPostsGroup, this.handlePuckPostBounce, undefined, this);
|
this.physics.add.collider(this.puck, this.goalPostsGroup, this.handlePuckPostBounce, undefined, this);
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
SHOOTING_ANGLE_THRESHOLD,
|
SHOOTING_ANGLE_THRESHOLD,
|
||||||
CLOSE_RANGE_DISTANCE,
|
CLOSE_RANGE_DISTANCE,
|
||||||
CLOSE_RANGE_ANGLE_THRESHOLD,
|
CLOSE_RANGE_ANGLE_THRESHOLD,
|
||||||
NEAR_PLAYER_DETECTION_RADIUS,
|
|
||||||
EVASION_ANGLE,
|
EVASION_ANGLE,
|
||||||
DEBUG
|
DEBUG
|
||||||
} from '../../../config/constants';
|
} 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
|
* Returns evasion target if threat detected, null otherwise
|
||||||
*
|
*
|
||||||
* The player will maintain evasion as long as an opponent is:
|
* 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
|
* Once an evasion direction is chosen, it persists until the opponent
|
||||||
* leaves the threat zone to prevent direction flipping.
|
* leaves the threat zone to prevent direction flipping.
|
||||||
*/
|
*/
|
||||||
private detectImmediateThreat(
|
private detectImmediateThreat(
|
||||||
player: Player,
|
player: Player,
|
||||||
gameState: GameState,
|
_gameState: GameState,
|
||||||
opponentGoalX: number
|
opponentGoalX: number
|
||||||
): { approachAngle: number; evasionTargetX: number; evasionTargetY: number; evasionAngle: number } | null {
|
): { approachAngle: number; evasionTargetX: number; evasionTargetY: number; evasionAngle: number } | null {
|
||||||
// Get or create debug graphics for this player
|
// Get or create debug graphics for this player
|
||||||
@ -126,7 +126,7 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
|||||||
const goalY = 0;
|
const goalY = 0;
|
||||||
const toGoalAngle = Math.atan2(goalY - player.gameY, opponentGoalX - player.gameX);
|
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) {
|
if (DEBUG && debugGraphics) {
|
||||||
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
||||||
const goalScreen = CoordinateUtils.gameToScreen(player.scene, opponentGoalX, goalY);
|
const goalScreen = CoordinateUtils.gameToScreen(player.scene, opponentGoalX, goalY);
|
||||||
@ -134,131 +134,119 @@ export class PuckCarrierBehavior extends BehaviorNode {
|
|||||||
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, goalScreen.x, goalScreen.y);
|
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, goalScreen.x, goalScreen.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all opponents
|
// Get detected opponents (physics-based with angle filtering)
|
||||||
const opponents = gameState.allPlayers.filter((p: Player) => p.team !== player.team);
|
const detectedOpponents = player.getDetectedOpponents();
|
||||||
|
|
||||||
// Check if we have any active evasions that are still valid
|
// Check if we have any active evasions that are still valid
|
||||||
for (const [opponentId, evasion] of playerEvasions.entries()) {
|
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) {
|
if (opponent) {
|
||||||
const distanceToOpponent = MathUtils.distance(player.gameX, player.gameY, opponent.gameX, opponent.gameY);
|
// 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;
|
||||||
|
const evasionTargetY = player.gameY + Math.sin(evasionAngle) * evasionDistance;
|
||||||
|
|
||||||
// If opponent still in threat zone, maintain the evasion
|
// Debug: Draw active evasion
|
||||||
if (distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
if (DEBUG && debugGraphics) {
|
||||||
const evasionAngle = evasion.side === 'left' ? toGoalAngle + EVASION_ANGLE : toGoalAngle - EVASION_ANGLE;
|
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
||||||
const evasionDistance = 20;
|
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
||||||
const evasionTargetX = player.gameX + Math.cos(evasionAngle) * evasionDistance;
|
const evasionTargetScreen = CoordinateUtils.gameToScreen(player.scene, evasionTargetX, evasionTargetY);
|
||||||
const evasionTargetY = player.gameY + Math.sin(evasionAngle) * evasionDistance;
|
|
||||||
|
|
||||||
// Debug: Draw active evasion
|
// Highlight the opponent being evaded
|
||||||
if (DEBUG && debugGraphics) {
|
debugGraphics.lineStyle(3, 0xff0000, 0.8); // Red circle around threat
|
||||||
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 15);
|
||||||
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
|
||||||
const evasionTargetScreen = CoordinateUtils.gameToScreen(player.scene, evasionTargetX, evasionTargetY);
|
|
||||||
|
|
||||||
// Highlight the opponent being evaded
|
// Draw evasion direction arrow
|
||||||
debugGraphics.lineStyle(3, 0xff0000, 0.8); // Red circle around threat
|
debugGraphics.lineStyle(3, 0xffff00, 0.8); // Yellow arrow for evasion
|
||||||
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 15);
|
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, evasionTargetScreen.x, evasionTargetScreen.y);
|
||||||
|
|
||||||
// Draw evasion direction arrow
|
// Draw evasion side label
|
||||||
debugGraphics.lineStyle(3, 0xffff00, 0.8); // Yellow arrow for evasion
|
const labelText = `EVADE ${evasion.side.toUpperCase()}`;
|
||||||
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, evasionTargetScreen.x, evasionTargetScreen.y);
|
const label = player.scene.add.text(playerScreen.x + 20, playerScreen.y - 20, labelText, {
|
||||||
|
fontSize: '10px',
|
||||||
// Draw evasion side label
|
color: '#ffff00',
|
||||||
const labelText = `EVADE ${evasion.side.toUpperCase()}`;
|
backgroundColor: '#000000'
|
||||||
const label = player.scene.add.text(playerScreen.x + 20, playerScreen.y - 20, labelText, {
|
});
|
||||||
fontSize: '10px',
|
player.scene.time.delayedCall(50, () => label.destroy());
|
||||||
color: '#ffff00',
|
|
||||||
backgroundColor: '#000000'
|
|
||||||
});
|
|
||||||
player.scene.time.delayedCall(50, () => label.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
approachAngle: Math.atan2(opponent.gameY - player.gameY, opponent.gameX - player.gameX),
|
|
||||||
evasionTargetX,
|
|
||||||
evasionTargetY,
|
|
||||||
evasionAngle
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Opponent left threat zone, clear the evasion
|
|
||||||
playerEvasions.delete(opponentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
approachAngle: Math.atan2(opponent.gameY - player.gameY, opponent.gameX - player.gameX),
|
||||||
|
evasionTargetX,
|
||||||
|
evasionTargetY,
|
||||||
|
evasionAngle
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Opponent no longer exists, clear the evasion
|
// Opponent left threat zone, clear the evasion
|
||||||
playerEvasions.delete(opponentId);
|
playerEvasions.delete(opponentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No active evasions, check for new threats
|
// No active evasions, check for new threats
|
||||||
for (const opponent of opponents) {
|
for (const opponent of detectedOpponents) {
|
||||||
const distanceToOpponent = MathUtils.distance(player.gameX, player.gameY, opponent.gameX, opponent.gameY);
|
// Debug: Draw all detected opponents
|
||||||
|
if (DEBUG && debugGraphics) {
|
||||||
// Debug: Draw all opponents in threat zone
|
|
||||||
if (DEBUG && debugGraphics && distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
|
||||||
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
||||||
debugGraphics.lineStyle(2, 0xffa500, 0.6); // Orange circle
|
debugGraphics.lineStyle(2, 0xffa500, 0.6); // Orange circle
|
||||||
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 12);
|
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if opponent is within threat zone
|
// Calculate angle from player to opponent
|
||||||
if (distanceToOpponent <= NEAR_PLAYER_DETECTION_RADIUS) {
|
const angleToOpponent = Math.atan2(
|
||||||
// Calculate angle from player to opponent
|
opponent.gameY - player.gameY,
|
||||||
const angleToOpponent = Math.atan2(
|
opponent.gameX - player.gameX
|
||||||
opponent.gameY - player.gameY,
|
);
|
||||||
opponent.gameX - player.gameX
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate how aligned the opponent is with our goal direction
|
// Calculate how aligned the opponent is with our goal direction
|
||||||
const angleDiff = MathUtils.angleDifference(toGoalAngle, angleToOpponent);
|
const angleDiff = MathUtils.angleDifference(toGoalAngle, angleToOpponent);
|
||||||
|
|
||||||
// Debug: Show angle difference
|
// Debug: Show angle difference
|
||||||
|
if (DEBUG && debugGraphics) {
|
||||||
|
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
||||||
|
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
||||||
|
debugGraphics.lineStyle(1, 0xffffff, 0.4);
|
||||||
|
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, opponentScreen.x, opponentScreen.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If opponent is roughly in front of us (within 60 degrees of goal direction)
|
||||||
|
if (Math.abs(angleDiff) < Math.PI / 3) {
|
||||||
|
// Calculate which side the opponent is on relative to our path
|
||||||
|
const crossProduct = Math.sin(angleToOpponent - toGoalAngle);
|
||||||
|
const evasionSide: 'left' | 'right' = crossProduct > 0 ? 'left' : 'right';
|
||||||
|
|
||||||
|
// Store this evasion for consistency
|
||||||
|
playerEvasions.set(opponent.id, { angle: toGoalAngle, side: evasionSide });
|
||||||
|
|
||||||
|
// Calculate evasion angle
|
||||||
|
const evasionAngle = evasionSide === 'left' ? toGoalAngle + EVASION_ANGLE : toGoalAngle - EVASION_ANGLE;
|
||||||
|
|
||||||
|
// Calculate evasion target point far ahead (20 meters)
|
||||||
|
const evasionDistance = 20;
|
||||||
|
const evasionTargetX = player.gameX + Math.cos(evasionAngle) * evasionDistance;
|
||||||
|
const evasionTargetY = player.gameY + Math.sin(evasionAngle) * evasionDistance;
|
||||||
|
|
||||||
|
// Debug: Draw new threat detection
|
||||||
if (DEBUG && debugGraphics) {
|
if (DEBUG && debugGraphics) {
|
||||||
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
||||||
|
const evasionTargetScreen = CoordinateUtils.gameToScreen(player.scene, evasionTargetX, evasionTargetY);
|
||||||
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
||||||
debugGraphics.lineStyle(1, 0xffffff, 0.4);
|
|
||||||
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, opponentScreen.x, opponentScreen.y);
|
// Highlight new threat
|
||||||
|
debugGraphics.lineStyle(4, 0xff0000, 1); // Bright red for new threat
|
||||||
|
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 15);
|
||||||
|
|
||||||
|
// Draw new evasion direction
|
||||||
|
debugGraphics.lineStyle(3, 0x00ff00, 0.8); // Green arrow for new evasion
|
||||||
|
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, evasionTargetScreen.x, evasionTargetScreen.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If opponent is roughly in front of us (within 60 degrees of goal direction)
|
return {
|
||||||
if (Math.abs(angleDiff) < Math.PI / 3) {
|
approachAngle: angleToOpponent,
|
||||||
// Calculate which side the opponent is on relative to our path
|
evasionTargetX,
|
||||||
const crossProduct = Math.sin(angleToOpponent - toGoalAngle);
|
evasionTargetY,
|
||||||
const evasionSide: 'left' | 'right' = crossProduct > 0 ? 'left' : 'right';
|
evasionAngle
|
||||||
|
};
|
||||||
// Store this evasion for consistency
|
|
||||||
playerEvasions.set(opponent.id, { angle: toGoalAngle, side: evasionSide });
|
|
||||||
|
|
||||||
// Calculate evasion angle
|
|
||||||
const evasionAngle = evasionSide === 'left' ? toGoalAngle + EVASION_ANGLE : toGoalAngle - EVASION_ANGLE;
|
|
||||||
|
|
||||||
// Calculate evasion target point far ahead (20 meters)
|
|
||||||
const evasionDistance = 20;
|
|
||||||
const evasionTargetX = player.gameX + Math.cos(evasionAngle) * evasionDistance;
|
|
||||||
const evasionTargetY = player.gameY + Math.sin(evasionAngle) * evasionDistance;
|
|
||||||
|
|
||||||
// Debug: Draw new threat detection
|
|
||||||
if (DEBUG && debugGraphics) {
|
|
||||||
const playerScreen = CoordinateUtils.gameToScreen(player.scene, player.gameX, player.gameY);
|
|
||||||
const evasionTargetScreen = CoordinateUtils.gameToScreen(player.scene, evasionTargetX, evasionTargetY);
|
|
||||||
const opponentScreen = CoordinateUtils.gameToScreen(player.scene, opponent.gameX, opponent.gameY);
|
|
||||||
|
|
||||||
// Highlight new threat
|
|
||||||
debugGraphics.lineStyle(4, 0xff0000, 1); // Bright red for new threat
|
|
||||||
debugGraphics.strokeCircle(opponentScreen.x, opponentScreen.y, 15);
|
|
||||||
|
|
||||||
// Draw new evasion direction
|
|
||||||
debugGraphics.lineStyle(3, 0x00ff00, 0.8); // Green arrow for new evasion
|
|
||||||
debugGraphics.lineBetween(playerScreen.x, playerScreen.y, evasionTargetScreen.x, evasionTargetScreen.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
approachAngle: angleToOpponent,
|
|
||||||
evasionTargetX,
|
|
||||||
evasionTargetY,
|
|
||||||
evasionAngle
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user