Restructure behavior tree for scalability
- Separate base behavior nodes into BehaviorNodes.ts (fixes circular dependency) - Organize behaviors by role (goalie) and context (offensive/defensive/transition) - Add modular behavior subtrees: - GoalieBehavior: Track puck position near net - SkaterBehavior: Route to context-specific behaviors - PuckCarrierBehavior: Shoot/carry logic (Phase 4) - OffensiveSupportBehavior: Placeholder for Phase 5 - DefensiveBehavior: Placeholder for Phase 6 - TransitionBehavior: Chase loose puck - Maintain backward compatibility with static evaluatePlayer() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1504dba13e
commit
a3ffc94916
86
src/systems/BehaviorNodes.ts
Normal file
86
src/systems/BehaviorNodes.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import type { Player } from '../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../types/game';
|
||||
|
||||
/**
|
||||
* Base class for all behavior tree nodes
|
||||
*/
|
||||
export enum BehaviorStatus {
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILURE = 'FAILURE',
|
||||
RUNNING = 'RUNNING',
|
||||
}
|
||||
|
||||
export abstract class BehaviorNode {
|
||||
abstract tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector node - Returns SUCCESS on first child that succeeds (OR logic)
|
||||
* Used for choosing between alternative behaviors
|
||||
*/
|
||||
export class Selector extends BehaviorNode {
|
||||
constructor(private children: BehaviorNode[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction {
|
||||
for (const child of this.children) {
|
||||
const result = child.tick(player, gameState);
|
||||
// If child returns an action or non-FAILURE status, return it
|
||||
if (result !== BehaviorStatus.FAILURE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return BehaviorStatus.FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sequence node - Returns SUCCESS only if all children succeed (AND logic)
|
||||
* Used for chaining conditions and actions
|
||||
*/
|
||||
export class Sequence extends BehaviorNode {
|
||||
constructor(private children: BehaviorNode[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction {
|
||||
let lastResult: BehaviorStatus | PlayerAction = BehaviorStatus.SUCCESS;
|
||||
for (const child of this.children) {
|
||||
const result = child.tick(player, gameState);
|
||||
if (result !== BehaviorStatus.SUCCESS) {
|
||||
return result; // Return FAILURE, RUNNING, or PlayerAction
|
||||
}
|
||||
lastResult = result;
|
||||
}
|
||||
return lastResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition node - Evaluates a predicate function
|
||||
*/
|
||||
export class Condition extends BehaviorNode {
|
||||
constructor(private predicate: (player: Player, gameState: GameState) => boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus {
|
||||
return this.predicate(player, gameState) ? BehaviorStatus.SUCCESS : BehaviorStatus.FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action node - Executes a behavior function and returns a PlayerAction
|
||||
*/
|
||||
export class Action extends BehaviorNode {
|
||||
constructor(
|
||||
private action: (player: Player, gameState: GameState) => PlayerAction
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): PlayerAction {
|
||||
return this.action(player, gameState);
|
||||
}
|
||||
}
|
||||
@ -1,136 +1,60 @@
|
||||
import type { Player } from '../entities/Player';
|
||||
import type { Puck } from '../entities/Puck';
|
||||
import {
|
||||
GOAL_LINE_OFFSET,
|
||||
GOALIE_RANGE,
|
||||
SHOOTING_RANGE,
|
||||
SHOOTING_ANGLE_THRESHOLD
|
||||
} from '../config/constants';
|
||||
import { MathUtils } from '../utils/math';
|
||||
import type { GameState, PlayerAction } from '../types/game';
|
||||
import { BehaviorNode, Selector, Sequence, Condition } from './BehaviorNodes';
|
||||
import { GoalieBehavior } from './behaviors/GoalieBehavior';
|
||||
import { SkaterBehavior } from './behaviors/SkaterBehavior';
|
||||
|
||||
// Re-export base classes for convenience
|
||||
export { BehaviorNode, BehaviorStatus, Selector, Sequence, Condition, Action } from './BehaviorNodes';
|
||||
|
||||
/**
|
||||
* Simple behavior tree for player decision making
|
||||
* BehaviorTree system for AI decision-making
|
||||
*
|
||||
* This system uses a behavior tree pattern to make tactical decisions for players.
|
||||
* The tree is evaluated every tick (60 FPS) and produces actions based on:
|
||||
* - Game state (possession, positions, threats)
|
||||
* - Player attributes (Hockey IQ, Skill, Speed)
|
||||
* - Tactical context (offensive/defensive situation)
|
||||
*
|
||||
* The tree is structured by role (goalie vs skater) at the top level,
|
||||
* then by game context (offensive/defensive/transition) for skaters.
|
||||
*
|
||||
* @example
|
||||
* const tree = new BehaviorTree(player);
|
||||
* const action = tree.tick(gameState); // Called every frame in GameScene.update()
|
||||
*/
|
||||
export class BehaviorTree {
|
||||
private root: BehaviorNode;
|
||||
|
||||
constructor(private player: Player) {
|
||||
// Root selector: Choose between goalie and skater behaviors based on position
|
||||
this.root = new Selector([
|
||||
// Goalie behavior (checks position internally)
|
||||
new Sequence([
|
||||
new Condition((p) => p.playerPosition === 'G'),
|
||||
new GoalieBehavior()
|
||||
]),
|
||||
// Skater behavior (default for all other positions)
|
||||
new SkaterBehavior()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate player behavior and return action
|
||||
* Evaluates the behavior tree and returns the next action for the player
|
||||
* Called every frame (60 FPS)
|
||||
*/
|
||||
public tick(gameState: GameState): PlayerAction {
|
||||
const result = this.root.tick(this.player, gameState);
|
||||
// Result should always be a PlayerAction from our structure
|
||||
return result as PlayerAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to evaluate any player
|
||||
* This maintains backward compatibility with existing code
|
||||
*/
|
||||
public static evaluatePlayer(player: Player, gameState: GameState): PlayerAction {
|
||||
const { puck } = gameState;
|
||||
|
||||
// Check if player is goalie
|
||||
if (player.playerPosition === 'G') {
|
||||
return this.goalieLogic(player, puck);
|
||||
}
|
||||
|
||||
// Check if player has puck
|
||||
if (puck.carrier === player.id) {
|
||||
return this.skateWithPuck(player, puck);
|
||||
}
|
||||
|
||||
// Check if puck is loose
|
||||
if (puck.state === 'loose') {
|
||||
return this.chasePuck(player, puck);
|
||||
}
|
||||
|
||||
// Default: idle (stay at current position)
|
||||
return {
|
||||
type: 'idle',
|
||||
targetX: player.gameX,
|
||||
targetY: player.gameY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Goalie stays near net and tracks puck Y position
|
||||
*/
|
||||
private static goalieLogic(player: Player, puck: Puck): PlayerAction {
|
||||
// Stay near goal line
|
||||
const goalX = player.team === 'home' ? -GOAL_LINE_OFFSET : GOAL_LINE_OFFSET;
|
||||
|
||||
// Track puck Y position (clamped to goal width)
|
||||
const targetY = Math.max(-GOALIE_RANGE, Math.min(GOALIE_RANGE, puck.gameY));
|
||||
|
||||
return {
|
||||
type: 'move',
|
||||
targetX: goalX,
|
||||
targetY: targetY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Chase loose puck
|
||||
*/
|
||||
private static chasePuck(_player: Player, puck: Puck): PlayerAction {
|
||||
return {
|
||||
type: 'chase_puck',
|
||||
targetX: puck.gameX,
|
||||
targetY: puck.gameY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Skate with puck toward opponent's goal
|
||||
*/
|
||||
private static skateWithPuck(player: Player, _puck: Puck): PlayerAction {
|
||||
// Determine opponent's goal X position
|
||||
const opponentGoalX = player.team === 'home' ? GOAL_LINE_OFFSET : -GOAL_LINE_OFFSET;
|
||||
|
||||
// Check if player has a good shot opportunity
|
||||
if (this.hasGoodShot(player)) {
|
||||
console.log(`${player.id} is shooting!`);
|
||||
return {
|
||||
type: 'shoot',
|
||||
targetX: opponentGoalX,
|
||||
targetY: 0 // Aim for center of net
|
||||
};
|
||||
}
|
||||
|
||||
// Skate toward opponent's net (slightly randomized Y to avoid going straight)
|
||||
return {
|
||||
type: 'skate_with_puck',
|
||||
targetX: opponentGoalX,
|
||||
targetY: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if player has a good shooting opportunity
|
||||
*/
|
||||
private static hasGoodShot(player: Player): boolean {
|
||||
// Determine opponent's goal position
|
||||
const opponentGoalX = player.team === 'home' ? GOAL_LINE_OFFSET : -GOAL_LINE_OFFSET;
|
||||
const goalY = 0;
|
||||
|
||||
// Calculate distance to goal
|
||||
const distance = MathUtils.distance(player.gameX, player.gameY, opponentGoalX, goalY);
|
||||
|
||||
// Check if within shooting range
|
||||
if (distance >= SHOOTING_RANGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate angle to goal (in radians)
|
||||
const dx = opponentGoalX - player.gameX;
|
||||
const dy = goalY - player.gameY;
|
||||
const angleToGoal = Math.atan2(dy, dx);
|
||||
|
||||
// Calculate player's direction of movement
|
||||
const moveX = player.targetX - player.gameX;
|
||||
const moveY = player.targetY - player.gameY;
|
||||
const angleOfMovement = Math.atan2(moveY, moveX);
|
||||
|
||||
// Calculate angle difference
|
||||
let angleDiff = Math.abs(angleToGoal - angleOfMovement);
|
||||
// Normalize to 0-π
|
||||
if (angleDiff > Math.PI) {
|
||||
angleDiff = 2 * Math.PI - angleDiff;
|
||||
}
|
||||
|
||||
// Good angle if within threshold
|
||||
const reasonableAngle = angleDiff < SHOOTING_ANGLE_THRESHOLD;
|
||||
|
||||
return reasonableAngle;
|
||||
const tree = new BehaviorTree(player);
|
||||
return tree.tick(gameState);
|
||||
}
|
||||
}
|
||||
|
||||
32
src/systems/behaviors/GoalieBehavior.ts
Normal file
32
src/systems/behaviors/GoalieBehavior.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { BehaviorNode } from '../BehaviorNodes';
|
||||
import type { Player } from '../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../types/game';
|
||||
import { GOAL_LINE_OFFSET, GOALIE_RANGE } from '../../config/constants';
|
||||
|
||||
/**
|
||||
* Goalie-specific behavior tree
|
||||
*
|
||||
* Goalies have unique behaviors:
|
||||
* - Stay near goal line
|
||||
* - Track puck Y position
|
||||
* - Challenge shooters (future)
|
||||
* - Make saves (future)
|
||||
*/
|
||||
export class GoalieBehavior extends BehaviorNode {
|
||||
tick(player: Player, gameState: GameState): PlayerAction {
|
||||
// Stay near goal line
|
||||
const goalX = player.team === 'home' ? -GOAL_LINE_OFFSET : GOAL_LINE_OFFSET;
|
||||
|
||||
// Track puck Y position (clamped to goalie range)
|
||||
const targetY = Math.max(
|
||||
-GOALIE_RANGE,
|
||||
Math.min(GOALIE_RANGE, gameState.puck.gameY)
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'move',
|
||||
targetX: goalX,
|
||||
targetY: targetY
|
||||
};
|
||||
}
|
||||
}
|
||||
39
src/systems/behaviors/SkaterBehavior.ts
Normal file
39
src/systems/behaviors/SkaterBehavior.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { BehaviorNode, Selector } from '../BehaviorNodes';
|
||||
import type { Player } from '../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../types/game';
|
||||
import { PuckCarrierBehavior } from './offensive/PuckCarrierBehavior';
|
||||
import { OffensiveSupportBehavior } from './offensive/OffensiveSupportBehavior';
|
||||
import { DefensiveBehavior } from './defensive/DefensiveBehavior';
|
||||
import { TransitionBehavior } from './TransitionBehavior';
|
||||
|
||||
/**
|
||||
* Skater behavior tree - Context-based decision making
|
||||
*
|
||||
* The selector evaluates children in priority order:
|
||||
* 1. PuckCarrier - If this player has the puck (highest priority)
|
||||
* 2. OffensiveSupport - If teammate has puck (support offense)
|
||||
* 3. Defensive - If opponent has puck (play defense)
|
||||
* 4. Transition - Loose puck or neutral situations (fallback)
|
||||
*
|
||||
* Each subtree handles its own conditions and returns FAILURE if not applicable,
|
||||
* allowing the selector to try the next branch.
|
||||
*/
|
||||
export class SkaterBehavior extends BehaviorNode {
|
||||
private tree: Selector;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Build context-based behavior tree
|
||||
this.tree = new Selector([
|
||||
new PuckCarrierBehavior(), // Has possession
|
||||
new OffensiveSupportBehavior(), // Team has possession
|
||||
new DefensiveBehavior(), // Opponent has possession
|
||||
new TransitionBehavior(), // Loose puck/neutral (always succeeds as fallback)
|
||||
]);
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): PlayerAction {
|
||||
return this.tree.tick(player, gameState) as PlayerAction;
|
||||
}
|
||||
}
|
||||
46
src/systems/behaviors/TransitionBehavior.ts
Normal file
46
src/systems/behaviors/TransitionBehavior.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { BehaviorNode, Selector, Action } from '../BehaviorNodes';
|
||||
import type { Player } from '../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../types/game';
|
||||
|
||||
/**
|
||||
* Transition behavior - Loose puck and neutral situations
|
||||
*
|
||||
* This behavior handles:
|
||||
* 1. Chasing loose pucks
|
||||
* 2. Neutral zone positioning
|
||||
* 3. Faceoff positioning (future)
|
||||
* 4. Line changes (future)
|
||||
*
|
||||
* This should always succeed as the fallback behavior
|
||||
*/
|
||||
export class TransitionBehavior extends BehaviorNode {
|
||||
private tree: BehaviorNode;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.tree = new Selector([
|
||||
// Chase loose puck
|
||||
new Action((player, gameState) => {
|
||||
if (gameState.puck.state === 'loose') {
|
||||
return {
|
||||
type: 'chase_puck',
|
||||
targetX: gameState.puck.gameX,
|
||||
targetY: gameState.puck.gameY
|
||||
};
|
||||
}
|
||||
|
||||
// If not loose puck, idle at current position (fallback)
|
||||
return {
|
||||
type: 'idle',
|
||||
targetX: player.gameX,
|
||||
targetY: player.gameY
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): PlayerAction {
|
||||
return this.tree.tick(player, gameState) as PlayerAction;
|
||||
}
|
||||
}
|
||||
47
src/systems/behaviors/defensive/DefensiveBehavior.ts
Normal file
47
src/systems/behaviors/defensive/DefensiveBehavior.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { BehaviorNode, Sequence, Condition, Action, BehaviorStatus } from '../../BehaviorNodes';
|
||||
import type { Player } from '../../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../../types/game';
|
||||
|
||||
/**
|
||||
* Defensive behavior (Phase 6 - Future)
|
||||
*
|
||||
* When opponent has the puck, this player should:
|
||||
* 1. Apply pressure to puck carrier
|
||||
* 2. Block passing lanes
|
||||
* 3. Cover open attackers
|
||||
* 4. Backcheck to defensive zone
|
||||
* 5. Position for shot blocks
|
||||
*
|
||||
* Currently: Placeholder that returns FAILURE to pass control to next behavior
|
||||
*/
|
||||
export class DefensiveBehavior extends BehaviorNode {
|
||||
private tree: BehaviorNode;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.tree = new Sequence([
|
||||
// Check if opponent has puck
|
||||
new Condition((player, gameState) => {
|
||||
const { puck } = gameState;
|
||||
if (puck.state !== 'carried') return false;
|
||||
if (puck.carrier === player.id) return false;
|
||||
|
||||
// TODO: Check if carrier is opponent (requires team roster access)
|
||||
// For now, just fail - will implement in Phase 6
|
||||
return false;
|
||||
}),
|
||||
|
||||
// Defensive action (future implementation)
|
||||
new Action((player) => ({
|
||||
type: 'idle',
|
||||
targetX: player.gameX,
|
||||
targetY: player.gameY
|
||||
}))
|
||||
]);
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction {
|
||||
return this.tree.tick(player, gameState);
|
||||
}
|
||||
}
|
||||
46
src/systems/behaviors/offensive/OffensiveSupportBehavior.ts
Normal file
46
src/systems/behaviors/offensive/OffensiveSupportBehavior.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { BehaviorNode, Sequence, Condition, Action, BehaviorStatus } from '../../BehaviorNodes';
|
||||
import type { Player } from '../../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../../types/game';
|
||||
|
||||
/**
|
||||
* Offensive support behavior (Phase 5 - Future)
|
||||
*
|
||||
* When a teammate has the puck, this player should:
|
||||
* 1. Get open for a pass
|
||||
* 2. Move to scoring areas
|
||||
* 3. Screen the goalie
|
||||
* 4. Provide passing options
|
||||
*
|
||||
* Currently: Placeholder that returns FAILURE to pass control to next behavior
|
||||
*/
|
||||
export class OffensiveSupportBehavior extends BehaviorNode {
|
||||
private tree: BehaviorNode;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.tree = new Sequence([
|
||||
// Check if teammate has puck (not this player, not opponent)
|
||||
new Condition((player, gameState) => {
|
||||
const { puck } = gameState;
|
||||
if (puck.state !== 'carried') return false;
|
||||
if (puck.carrier === player.id) return false;
|
||||
|
||||
// TODO: Check if carrier is teammate (requires team roster access)
|
||||
// For now, just fail - will implement in Phase 5
|
||||
return false;
|
||||
}),
|
||||
|
||||
// Action to support offense (future implementation)
|
||||
new Action((player) => ({
|
||||
type: 'idle',
|
||||
targetX: player.gameX,
|
||||
targetY: player.gameY
|
||||
}))
|
||||
]);
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction {
|
||||
return this.tree.tick(player, gameState);
|
||||
}
|
||||
}
|
||||
103
src/systems/behaviors/offensive/PuckCarrierBehavior.ts
Normal file
103
src/systems/behaviors/offensive/PuckCarrierBehavior.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { BehaviorNode, Sequence, Condition, Action, BehaviorStatus } from '../../BehaviorNodes';
|
||||
import type { Player } from '../../../entities/Player';
|
||||
import type { GameState, PlayerAction } from '../../../types/game';
|
||||
import {
|
||||
GOAL_LINE_OFFSET,
|
||||
SHOOTING_RANGE,
|
||||
SHOOTING_ANGLE_THRESHOLD
|
||||
} from '../../../config/constants';
|
||||
import { MathUtils } from '../../../utils/math';
|
||||
|
||||
/**
|
||||
* Puck carrier behavior tree (Phase 4)
|
||||
*
|
||||
* Decision priority when carrying the puck:
|
||||
* 1. Shoot if good opportunity
|
||||
* 2. Pass to open teammate (future - Phase 4)
|
||||
* 3. Carry puck toward net (default)
|
||||
*
|
||||
* Future enhancements:
|
||||
* - Deke/stickhandle under pressure
|
||||
* - One-timer pass opportunities
|
||||
* - Look for trailer for pass
|
||||
*/
|
||||
export class PuckCarrierBehavior extends BehaviorNode {
|
||||
private tree: BehaviorNode;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Build decision tree for puck carrier
|
||||
this.tree = new Sequence([
|
||||
// First check if this player has the puck
|
||||
new Condition((player, gameState) => gameState.puck.carrier === player.id),
|
||||
|
||||
// Then choose action (in future, this will be a Selector with shoot/pass/carry options)
|
||||
new Action((player, gameState) => this.decidePuckCarrierAction(player, gameState))
|
||||
]);
|
||||
}
|
||||
|
||||
tick(player: Player, gameState: GameState): BehaviorStatus | PlayerAction {
|
||||
return this.tree.tick(player, gameState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide what to do with the puck
|
||||
* Currently: Shoot if good opportunity, otherwise carry toward net
|
||||
*/
|
||||
private decidePuckCarrierAction(player: Player, _gameState: GameState): PlayerAction {
|
||||
const opponentGoalX = player.team === 'home' ? GOAL_LINE_OFFSET : -GOAL_LINE_OFFSET;
|
||||
|
||||
// Check for shooting opportunity
|
||||
if (this.hasGoodShot(player, opponentGoalX)) {
|
||||
console.log(`${player.id} is shooting!`);
|
||||
return {
|
||||
type: 'shoot',
|
||||
targetX: opponentGoalX,
|
||||
targetY: 0 // Aim for center of net
|
||||
};
|
||||
}
|
||||
|
||||
// Default: Carry puck toward opponent's net
|
||||
return {
|
||||
type: 'skate_with_puck',
|
||||
targetX: opponentGoalX,
|
||||
targetY: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if player has a good shooting opportunity
|
||||
*/
|
||||
private hasGoodShot(player: Player, opponentGoalX: number): boolean {
|
||||
const goalY = 0;
|
||||
|
||||
// Calculate distance to goal
|
||||
const distance = MathUtils.distance(player.gameX, player.gameY, opponentGoalX, goalY);
|
||||
|
||||
// Check if within shooting range
|
||||
if (distance >= SHOOTING_RANGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate angle to goal (in radians)
|
||||
const dx = opponentGoalX - player.gameX;
|
||||
const dy = goalY - player.gameY;
|
||||
const angleToGoal = Math.atan2(dy, dx);
|
||||
|
||||
// Calculate player's direction of movement
|
||||
const moveX = player.targetX - player.gameX;
|
||||
const moveY = player.targetY - player.gameY;
|
||||
const angleOfMovement = Math.atan2(moveY, moveX);
|
||||
|
||||
// Calculate angle difference
|
||||
let angleDiff = Math.abs(angleToGoal - angleOfMovement);
|
||||
// Normalize to 0-π
|
||||
if (angleDiff > Math.PI) {
|
||||
angleDiff = 2 * Math.PI - angleDiff;
|
||||
}
|
||||
|
||||
// Good angle if within threshold
|
||||
return angleDiff < SHOOTING_ANGLE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user