init player

This commit is contained in:
Pierre Wessman 2025-10-01 14:51:24 +00:00
parent 93cb732110
commit d2fb60df8d
5 changed files with 435 additions and 9 deletions

161
src/entities/Player.ts Normal file
View 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;
}
}

View File

@ -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;
}
} }

View File

@ -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);
}
} }
} }

View File

@ -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
View 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;
}
}