init player
This commit is contained in:
parent
93cb732110
commit
d2fb60df8d
161
src/entities/Player.ts
Normal file
161
src/entities/Player.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import { SCALE } from '../config/constants';
|
||||||
|
|
||||||
|
export type PlayerPosition = 'LW' | 'C' | 'RW' | 'LD' | 'RD' | 'G';
|
||||||
|
export type TeamSide = 'home' | 'away';
|
||||||
|
export type PlayerState = 'offensive' | 'defensive';
|
||||||
|
|
||||||
|
export interface PlayerAttributes {
|
||||||
|
speed: number; // 0-100: movement speed
|
||||||
|
skill: number; // 0-100: pass/shot accuracy, decision quality
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Player extends Phaser.GameObjects.Container {
|
||||||
|
declare body: Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Player identity
|
||||||
|
public id: string;
|
||||||
|
public team: TeamSide;
|
||||||
|
public playerPosition: PlayerPosition;
|
||||||
|
|
||||||
|
// Position in game coordinates (meters)
|
||||||
|
public gameX: number;
|
||||||
|
public gameY: number;
|
||||||
|
|
||||||
|
// Target position (where player wants to move)
|
||||||
|
public targetX: number;
|
||||||
|
public targetY: number;
|
||||||
|
|
||||||
|
// Player attributes
|
||||||
|
public attributes: PlayerAttributes;
|
||||||
|
|
||||||
|
// Player state
|
||||||
|
public state: PlayerState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
id: string,
|
||||||
|
team: TeamSide,
|
||||||
|
playerPosition: PlayerPosition,
|
||||||
|
gameX: number,
|
||||||
|
gameY: number,
|
||||||
|
attributes: PlayerAttributes
|
||||||
|
) {
|
||||||
|
// Convert game coordinates to screen coordinates
|
||||||
|
const screenX = (scene.game.config.width as number) / 2 + gameX * SCALE;
|
||||||
|
const screenY = (scene.game.config.height as number) / 2 - gameY * SCALE;
|
||||||
|
|
||||||
|
super(scene, screenX, screenY);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.team = team;
|
||||||
|
this.playerPosition = playerPosition;
|
||||||
|
this.gameX = gameX;
|
||||||
|
this.gameY = gameY;
|
||||||
|
this.targetX = gameX;
|
||||||
|
this.targetY = gameY;
|
||||||
|
this.attributes = attributes;
|
||||||
|
this.state = 'defensive';
|
||||||
|
|
||||||
|
// Add to scene
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
this.body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Set physics body (circular, centered on container)
|
||||||
|
const radius = this.playerPosition === 'G' ? 12 : 10;
|
||||||
|
this.body.setCircle(radius);
|
||||||
|
this.body.setOffset(-radius, -radius); // Center the body on the container
|
||||||
|
this.body.setCollideWorldBounds(true);
|
||||||
|
|
||||||
|
this.createSprite();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSprite() {
|
||||||
|
const graphics = this.scene.add.graphics();
|
||||||
|
|
||||||
|
// Determine color based on team
|
||||||
|
const color = this.team === 'home' ? 0x0000ff : 0xff0000;
|
||||||
|
|
||||||
|
// Make goalie larger
|
||||||
|
const radius = this.playerPosition === 'G' ? 12 : 10;
|
||||||
|
|
||||||
|
// Draw player circle
|
||||||
|
graphics.fillStyle(color, 1);
|
||||||
|
graphics.fillCircle(0, 0, radius);
|
||||||
|
|
||||||
|
// Add white outline
|
||||||
|
graphics.lineStyle(2, 0xffffff, 1);
|
||||||
|
graphics.strokeCircle(0, 0, radius);
|
||||||
|
|
||||||
|
// Add position label
|
||||||
|
const label = this.scene.add.text(0, 0, this.playerPosition, {
|
||||||
|
fontSize: '8px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontStyle: 'bold'
|
||||||
|
});
|
||||||
|
label.setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
|
this.add([graphics, label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update player position in game coordinates (meters)
|
||||||
|
*/
|
||||||
|
public setGamePosition(gameX: number, gameY: number) {
|
||||||
|
this.gameX = gameX;
|
||||||
|
this.gameY = gameY;
|
||||||
|
|
||||||
|
// Convert to screen coordinates
|
||||||
|
const centerX = (this.scene.game.config.width as number) / 2;
|
||||||
|
const centerY = (this.scene.game.config.height as number) / 2;
|
||||||
|
|
||||||
|
this.setPosition(
|
||||||
|
centerX + gameX * SCALE,
|
||||||
|
centerY - gameY * SCALE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set target position for movement
|
||||||
|
*/
|
||||||
|
public setTarget(targetX: number, targetY: number) {
|
||||||
|
this.targetX = targetX;
|
||||||
|
this.targetY = targetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update player movement each frame
|
||||||
|
*/
|
||||||
|
public update(_delta: number) {
|
||||||
|
// Calculate distance to target
|
||||||
|
const dx = this.targetX - this.gameX;
|
||||||
|
const dy = this.targetY - this.gameY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// If close enough to target, stop
|
||||||
|
if (distance < 0.1) {
|
||||||
|
this.body.setVelocity(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate velocity based on speed attribute
|
||||||
|
// speed attribute (0-100) maps to actual m/s (e.g., 80 -> 8 m/s)
|
||||||
|
const speed = (this.attributes.speed / 10) * SCALE; // Convert to pixels/s
|
||||||
|
|
||||||
|
// Normalize direction and apply speed
|
||||||
|
const dirX = dx / distance;
|
||||||
|
const dirY = dy / distance;
|
||||||
|
|
||||||
|
this.body.setVelocity(dirX * speed, -dirY * speed); // Negative Y because screen coords
|
||||||
|
|
||||||
|
// Update game position based on physics body center
|
||||||
|
const centerX = (this.scene.game.config.width as number) / 2;
|
||||||
|
const centerY = (this.scene.game.config.height as number) / 2;
|
||||||
|
const bodyX = this.body.x + this.body.width / 2;
|
||||||
|
const bodyY = this.body.y + this.body.height / 2;
|
||||||
|
this.gameX = (bodyX - centerX) / SCALE;
|
||||||
|
this.gameY = -(bodyY - centerY) / SCALE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,13 +41,8 @@ export class Puck extends Phaser.GameObjects.Container {
|
|||||||
// Set max velocity (allow up to x m/s)
|
// Set max velocity (allow up to x m/s)
|
||||||
this.body.setMaxVelocity(50 * SCALE, 50 * SCALE);
|
this.body.setMaxVelocity(50 * SCALE, 50 * SCALE);
|
||||||
|
|
||||||
// Set initial velocity (5 m/s in a random direction for testing)
|
// Set initial velocity to 0 (stationary)
|
||||||
const angle = Math.PI * .0; //Math.random() * Math.PI * 2;
|
this.body.setVelocity(0, 0);
|
||||||
const speed = 100 * SCALE; // 5 m/s converted to pixels/s
|
|
||||||
this.body.setVelocity(
|
|
||||||
Math.cos(angle) * speed,
|
|
||||||
Math.sin(angle) * speed
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add bounce to keep it moving
|
// Add bounce to keep it moving
|
||||||
this.body.setBounce(1, 1);
|
this.body.setBounce(1, 1);
|
||||||
@ -113,4 +108,19 @@ export class Puck extends Phaser.GameObjects.Container {
|
|||||||
public getGamePosition(): { x: number; y: number } {
|
public getGamePosition(): { x: number; y: number } {
|
||||||
return { x: this.gameX, y: this.gameY };
|
return { x: this.gameX, y: this.gameY };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update puck game coordinates based on physics body position
|
||||||
|
*/
|
||||||
|
public update() {
|
||||||
|
const centerX = (this.scene.game.config.width as number) / 2;
|
||||||
|
const centerY = (this.scene.game.config.height as number) / 2;
|
||||||
|
|
||||||
|
// Use body center position (body.x/y is top-left, need to account for that)
|
||||||
|
const bodyX = this.body.x + this.body.width / 2;
|
||||||
|
const bodyY = this.body.y + this.body.height / 2;
|
||||||
|
|
||||||
|
this.gameX = (bodyX - centerX) / SCALE;
|
||||||
|
this.gameY = -(bodyY - centerY) / SCALE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,11 +13,14 @@ import {
|
|||||||
} from '../config/constants';
|
} from '../config/constants';
|
||||||
import { Goal } from './Goal';
|
import { Goal } from './Goal';
|
||||||
import { Puck } from '../entities/Puck';
|
import { Puck } from '../entities/Puck';
|
||||||
|
import { Player } from '../entities/Player';
|
||||||
|
import { BehaviorTree } from '../systems/BehaviorTree';
|
||||||
|
|
||||||
export class GameScene extends Phaser.Scene {
|
export class GameScene extends Phaser.Scene {
|
||||||
private leftGoal!: Goal;
|
private leftGoal!: Goal;
|
||||||
private rightGoal!: Goal;
|
private rightGoal!: Goal;
|
||||||
private puck!: Puck;
|
private puck!: Puck;
|
||||||
|
private players: Player[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'GameScene' });
|
super({ key: 'GameScene' });
|
||||||
@ -27,13 +30,33 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.drawRink();
|
this.drawRink();
|
||||||
this.createGoals();
|
this.createGoals();
|
||||||
this.createPuck();
|
this.createPuck();
|
||||||
|
this.createPlayers();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPlayers() {
|
||||||
|
// Create one home center at (-10, 0) - left side of center ice
|
||||||
|
const homeCenter = new Player(
|
||||||
|
this,
|
||||||
|
'home-C',
|
||||||
|
'home',
|
||||||
|
'C',
|
||||||
|
-10,
|
||||||
|
0,
|
||||||
|
{ speed: 80, skill: 75 }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.players.push(homeCenter);
|
||||||
|
}
|
||||||
|
|
||||||
private setupEventListeners() {
|
private setupEventListeners() {
|
||||||
// Listen for goal events
|
// Listen for goal events
|
||||||
this.events.on('goal', (data: { team: string; goal: string }) => {
|
this.events.on('goal', (data: { team: string; goal: string }) => {
|
||||||
console.log(`[GameScene] Goal scored by ${data.team} team in ${data.goal} goal`);
|
console.log(`[GameScene] Goal scored by ${data.team} team in ${data.goal} goal`);
|
||||||
|
|
||||||
|
// Stop the puck (caught by net)
|
||||||
|
this.puck.body.setVelocity(0, 0);
|
||||||
|
|
||||||
// Future: update score, trigger celebration, reset to faceoff, etc.
|
// Future: update score, trigger celebration, reset to faceoff, etc.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,9 +133,102 @@ export class GameScene extends Phaser.Scene {
|
|||||||
graphics.strokeRoundedRect(2, 2, RINK_LENGTH * SCALE - 4, RINK_WIDTH * SCALE - 4, RINK_CORNER_RADIUS * SCALE);
|
graphics.strokeRoundedRect(2, 2, RINK_LENGTH * SCALE - 4, RINK_WIDTH * SCALE - 4, RINK_CORNER_RADIUS * SCALE);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update(_time: number, delta: number) {
|
||||||
|
// Update puck position
|
||||||
|
this.puck.update();
|
||||||
|
|
||||||
|
// Check for puck pickup
|
||||||
|
this.checkPuckPickup();
|
||||||
|
|
||||||
// Check for goals
|
// Check for goals
|
||||||
this.leftGoal.checkGoal(this.puck);
|
this.leftGoal.checkGoal(this.puck);
|
||||||
this.rightGoal.checkGoal(this.puck);
|
this.rightGoal.checkGoal(this.puck);
|
||||||
|
|
||||||
|
// Build game state
|
||||||
|
const gameState = {
|
||||||
|
puck: this.puck,
|
||||||
|
allPlayers: this.players
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update all players with behavior tree decisions
|
||||||
|
this.players.forEach(player => {
|
||||||
|
// Evaluate behavior tree to get action
|
||||||
|
const action = BehaviorTree.evaluatePlayer(player, gameState);
|
||||||
|
|
||||||
|
// Apply action to player
|
||||||
|
if (action.type === 'shoot') {
|
||||||
|
// Execute shot
|
||||||
|
this.executeShot(player, action.targetX!, action.targetY!);
|
||||||
|
} else if (action.type === 'move' || action.type === 'chase_puck' || action.type === 'skate_with_puck') {
|
||||||
|
if (action.targetX !== undefined && action.targetY !== undefined) {
|
||||||
|
player.setTarget(action.targetX, action.targetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player movement
|
||||||
|
player.update(delta);
|
||||||
|
|
||||||
|
// If player has puck, update puck position (in front of player)
|
||||||
|
if (this.puck.carrier === player.id) {
|
||||||
|
// Calculate direction player is moving
|
||||||
|
const dx = player.targetX - player.gameX;
|
||||||
|
const dy = player.targetY - player.gameY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance > 0.1) {
|
||||||
|
// Place puck 1 meter in front of player in direction of movement
|
||||||
|
const dirX = dx / distance;
|
||||||
|
const dirY = dy / distance;
|
||||||
|
this.puck.setGamePosition(
|
||||||
|
player.gameX + dirX * 1.0,
|
||||||
|
player.gameY + dirY * 1.0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If not moving, keep puck at player position
|
||||||
|
this.puck.setGamePosition(player.gameX, player.gameY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkPuckPickup() {
|
||||||
|
if (this.puck.state !== 'loose') return;
|
||||||
|
|
||||||
|
// Check each player's distance to puck
|
||||||
|
this.players.forEach(player => {
|
||||||
|
const dx = player.gameX - this.puck.gameX;
|
||||||
|
const dy = player.gameY - this.puck.gameY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// Pickup radius: 1.5 meters
|
||||||
|
if (distance < 1.5) {
|
||||||
|
this.puck.setCarrier(player.id, player.team);
|
||||||
|
console.log(`${player.id} picked up the puck`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeShot(player: Player, targetX: number, targetY: number) {
|
||||||
|
console.log(`${player.id} shoots toward (${targetX}, ${targetY})`);
|
||||||
|
|
||||||
|
// Release puck from player control
|
||||||
|
this.puck.setLoose();
|
||||||
|
|
||||||
|
// Calculate shot direction
|
||||||
|
const dx = targetX - player.gameX;
|
||||||
|
const dy = targetY - player.gameY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance > 0) {
|
||||||
|
// Normalize direction
|
||||||
|
const dirX = dx / distance;
|
||||||
|
const dirY = dy / distance;
|
||||||
|
|
||||||
|
// Shot speed: 30 m/s
|
||||||
|
const shotSpeed = 30 * SCALE;
|
||||||
|
|
||||||
|
// Apply velocity to puck
|
||||||
|
this.puck.body.setVelocity(dirX * shotSpeed, -dirY * shotSpeed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export class Goal extends Phaser.GameObjects.Container {
|
|||||||
const postThickness = 0.3 * SCALE; // Post thickness
|
const postThickness = 0.3 * SCALE; // Post thickness
|
||||||
const goalWidth = GOAL_WIDTH * SCALE; // Width of goal opening (top to bottom)
|
const goalWidth = GOAL_WIDTH * SCALE; // Width of goal opening (top to bottom)
|
||||||
const goalDepth = GOAL_DEPTH * SCALE; // Depth extending into zone
|
const goalDepth = GOAL_DEPTH * SCALE; // Depth extending into zone
|
||||||
const barThickness = 0.3 * SCALE; // Thicker bar to prevent high-speed puck tunneling
|
const barThickness = 0.4 * SCALE; // Thicker bar to prevent high-speed puck tunneling
|
||||||
|
|
||||||
// Create graphics for visual representation
|
// Create graphics for visual representation
|
||||||
const graphics = this.scene.add.graphics();
|
const graphics = this.scene.add.graphics();
|
||||||
|
|||||||
139
src/systems/BehaviorTree.ts
Normal file
139
src/systems/BehaviorTree.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import type { Player } from '../entities/Player';
|
||||||
|
import type { Puck } from '../entities/Puck';
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
puck: Puck;
|
||||||
|
allPlayers: Player[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAction {
|
||||||
|
type: 'move' | 'chase_puck' | 'skate_with_puck' | 'shoot' | 'idle';
|
||||||
|
targetX?: number;
|
||||||
|
targetY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple behavior tree for player decision making
|
||||||
|
*/
|
||||||
|
export class BehaviorTree {
|
||||||
|
/**
|
||||||
|
* Evaluate player behavior and return action
|
||||||
|
*/
|
||||||
|
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' ? -26 : 26;
|
||||||
|
|
||||||
|
// Track puck Y position (clamped to goal width)
|
||||||
|
const targetY = Math.max(-3, Math.min(3, 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' ? 26 : -26;
|
||||||
|
|
||||||
|
// 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' ? 26 : -26;
|
||||||
|
const goalY = 0;
|
||||||
|
|
||||||
|
// Calculate distance to goal
|
||||||
|
const dx = opponentGoalX - player.gameX;
|
||||||
|
const dy = goalY - player.gameY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// Check if within shooting range (< 10m)
|
||||||
|
if (distance >= 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate angle to goal (in radians)
|
||||||
|
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 45 degrees (π/4 radians) of goal
|
||||||
|
const reasonableAngle = angleDiff < Math.PI / 4;
|
||||||
|
|
||||||
|
return reasonableAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user