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)
|
||||
this.body.setMaxVelocity(50 * SCALE, 50 * SCALE);
|
||||
|
||||
// Set initial velocity (5 m/s in a random direction for testing)
|
||||
const angle = Math.PI * .0; //Math.random() * Math.PI * 2;
|
||||
const speed = 100 * SCALE; // 5 m/s converted to pixels/s
|
||||
this.body.setVelocity(
|
||||
Math.cos(angle) * speed,
|
||||
Math.sin(angle) * speed
|
||||
);
|
||||
// Set initial velocity to 0 (stationary)
|
||||
this.body.setVelocity(0, 0);
|
||||
|
||||
// Add bounce to keep it moving
|
||||
this.body.setBounce(1, 1);
|
||||
@ -113,4 +108,19 @@ export class Puck extends Phaser.GameObjects.Container {
|
||||
public getGamePosition(): { x: number; y: number } {
|
||||
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';
|
||||
import { Goal } from './Goal';
|
||||
import { Puck } from '../entities/Puck';
|
||||
import { Player } from '../entities/Player';
|
||||
import { BehaviorTree } from '../systems/BehaviorTree';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private leftGoal!: Goal;
|
||||
private rightGoal!: Goal;
|
||||
private puck!: Puck;
|
||||
private players: Player[] = [];
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
@ -27,13 +30,33 @@ export class GameScene extends Phaser.Scene {
|
||||
this.drawRink();
|
||||
this.createGoals();
|
||||
this.createPuck();
|
||||
this.createPlayers();
|
||||
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() {
|
||||
// Listen for goal events
|
||||
this.events.on('goal', (data: { team: string; goal: string }) => {
|
||||
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.
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
update() {
|
||||
update(_time: number, delta: number) {
|
||||
// Update puck position
|
||||
this.puck.update();
|
||||
|
||||
// Check for puck pickup
|
||||
this.checkPuckPickup();
|
||||
|
||||
// Check for goals
|
||||
this.leftGoal.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 goalWidth = GOAL_WIDTH * SCALE; // Width of goal opening (top to bottom)
|
||||
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
|
||||
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