Compare commits
No commits in common. "c81e500e185369cf4013bd09df08456715aad4a1" and "a3ffc9491603c032909cdd2ac87df1a8351cfd79" have entirely different histories.
c81e500e18
...
a3ffc94916
@ -3,9 +3,7 @@
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(pnpm run:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(git log:*)"
|
||||
"Bash(pnpm run:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@ -25,19 +25,17 @@ export const COLOR_GOAL_CREASE = 0x87ceeb;
|
||||
|
||||
// Game settings
|
||||
export const FPS = 60;
|
||||
export const DEBUG = false;
|
||||
export const DEBUG = true;
|
||||
|
||||
// Player constants
|
||||
export const PLAYER_RADIUS_GOALIE = 12; // pixels
|
||||
export const PLAYER_RADIUS_SKATER = 10; // pixels
|
||||
export const PLAYER_ROTATION_SPEED = 10; // radians per second
|
||||
export const PLAYER_ROTATION_SPEED = 1; // radians per second
|
||||
export const SPEED_SCALE_FACTOR = 10; // speed attribute (0-100) / 10 = m/s
|
||||
export const GOAL_DECELERATION_RATE = 0.9; // Speed multiplier reduction after goal
|
||||
|
||||
// Movement constants
|
||||
export const MOVEMENT_STOP_THRESHOLD = 0.1; // meters - stop moving when this close to target
|
||||
export const PLAYER_ACCELERATION = 10; // m/s² - how quickly player reaches max speed
|
||||
export const PLAYER_DECELERATION = 20; // m/s² - how quickly player stops
|
||||
|
||||
// Puck constants
|
||||
export const PUCK_RADIUS = 0.2; // meters (regulation hockey puck is ~7.6cm diameter, make it larger to be visible)
|
||||
@ -58,8 +56,3 @@ export const CENTER_DOT_RADIUS = 5; // pixels
|
||||
export const SHOOTING_RANGE = 10; // meters - max distance to attempt shot
|
||||
export const SHOOTING_ANGLE_THRESHOLD = Math.PI / 4; // radians (45 degrees)
|
||||
export const GOALIE_RANGE = 3; // meters - how far goalie moves from center
|
||||
|
||||
// Tackle constants
|
||||
export const TACKLE_SUCCESS_MODIFIER = 1; // Multiplier for tackle success calculation (balancing)
|
||||
export const TACKLE_PUCK_LOOSE_CHANCE = 0.6; // Chance puck becomes loose after tackle
|
||||
export const TACKLE_COOLDOWN = 1000; // ms - time between tackles for same player
|
||||
|
||||
@ -6,11 +6,7 @@ import {
|
||||
PLAYER_RADIUS_SKATER,
|
||||
SPEED_SCALE_FACTOR,
|
||||
MOVEMENT_STOP_THRESHOLD,
|
||||
GOAL_DECELERATION_RATE,
|
||||
DEBUG,
|
||||
TACKLE_COOLDOWN,
|
||||
PLAYER_ACCELERATION,
|
||||
PLAYER_DECELERATION
|
||||
GOAL_DECELERATION_RATE
|
||||
} from '../config/constants';
|
||||
import { CoordinateUtils } from '../utils/coordinates';
|
||||
import { MathUtils } from '../utils/math';
|
||||
@ -42,19 +38,6 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
// Rotation (angle in radians, 0 = facing right)
|
||||
private currentAngle: number = 0;
|
||||
|
||||
// Current speed (m/s in game coordinates)
|
||||
private currentSpeed: number = 0;
|
||||
|
||||
// Tackle cooldown tracking
|
||||
private lastTackleTime: number = 0;
|
||||
|
||||
// Debug visualizations
|
||||
private debugTargetGraphics?: Phaser.GameObjects.Graphics;
|
||||
private debugLineGraphics?: Phaser.GameObjects.Graphics;
|
||||
|
||||
// Direction indicator
|
||||
private directionIndicator!: Phaser.GameObjects.Graphics;
|
||||
|
||||
constructor(
|
||||
scene: Phaser.Scene,
|
||||
id: string,
|
||||
@ -79,10 +62,6 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
this.attributes = attributes;
|
||||
this.state = 'defensive';
|
||||
|
||||
// Initialize facing direction based on team
|
||||
// Home team (left side) faces right, Away team (right side) faces left
|
||||
this.currentAngle = this.team === 'home' ? 0 : Math.PI;
|
||||
|
||||
// Listen for goal events
|
||||
this.scene.events.on('goal', this.onGoal, this);
|
||||
|
||||
@ -99,12 +78,6 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
this.body.setCollideWorldBounds(true);
|
||||
|
||||
this.createSprite();
|
||||
|
||||
// Create debug graphics if DEBUG is enabled
|
||||
if (DEBUG) {
|
||||
this.debugTargetGraphics = scene.add.graphics();
|
||||
this.debugLineGraphics = scene.add.graphics();
|
||||
}
|
||||
}
|
||||
|
||||
private createSprite() {
|
||||
@ -132,28 +105,7 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
});
|
||||
label.setOrigin(0.5, 0.5);
|
||||
|
||||
// Create direction indicator (small line showing facing direction)
|
||||
this.directionIndicator = this.scene.add.graphics();
|
||||
this.updateDirectionIndicator();
|
||||
|
||||
this.add([graphics, label, this.directionIndicator]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the direction indicator to show current facing angle
|
||||
*/
|
||||
private updateDirectionIndicator() {
|
||||
this.directionIndicator.clear();
|
||||
|
||||
const radius = this.playerPosition === 'G' ? PLAYER_RADIUS_GOALIE : PLAYER_RADIUS_SKATER;
|
||||
|
||||
// Draw line pointing right (container rotation will orient it correctly)
|
||||
// Line goes from behind center to near the edge (0=center, 1=edge)
|
||||
const startX = -radius * -0.5; // Start
|
||||
const endX = radius * 1.0; // End
|
||||
|
||||
this.directionIndicator.lineStyle(3, 0x999999, 1);
|
||||
this.directionIndicator.lineBetween(startX, 0, endX, 0); // Horizontal line, rotation handled by container
|
||||
this.add([graphics, label]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,30 +150,15 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
* Update player movement each frame
|
||||
*/
|
||||
public update(delta: number) {
|
||||
const deltaSeconds = delta / 1000;
|
||||
|
||||
// Calculate distance to target
|
||||
const distance = MathUtils.distance(this.gameX, this.gameY, this.targetX, this.targetY);
|
||||
|
||||
// Calculate max speed based on player attributes (in m/s)
|
||||
const maxSpeedMS = (this.attributes.speed / SPEED_SCALE_FACTOR) * this.speedMultiplier;
|
||||
|
||||
// If close enough to target, decelerate to stop
|
||||
// If close enough to target, stop
|
||||
if (distance < MOVEMENT_STOP_THRESHOLD) {
|
||||
// Decelerate
|
||||
this.currentSpeed = Math.max(0, this.currentSpeed - PLAYER_DECELERATION * deltaSeconds);
|
||||
|
||||
if (this.currentSpeed < 0.1) {
|
||||
this.currentSpeed = 0;
|
||||
this.body.setVelocity(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue moving in current direction while decelerating
|
||||
const velX = Math.cos(this.currentAngle) * this.currentSpeed * SCALE;
|
||||
const velY = Math.sin(this.currentAngle) * this.currentSpeed * SCALE;
|
||||
this.body.setVelocity(velX, -velY);
|
||||
} else {
|
||||
const dx = this.targetX - this.gameX;
|
||||
const dy = this.targetY - this.gameY;
|
||||
|
||||
@ -229,10 +166,8 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
const targetAngle = Math.atan2(dy, dx);
|
||||
|
||||
// Smoothly rotate toward target angle
|
||||
// Scale rotation speed inversely with current speed (faster = wider turns)
|
||||
const speedRatio = maxSpeedMS > 0 ? this.currentSpeed / maxSpeedMS : 0;
|
||||
const turnPenalty = 1 - (speedRatio * 0.7); // At max speed, rotation is 30% of base
|
||||
const maxRotation = PLAYER_ROTATION_SPEED * turnPenalty * deltaSeconds;
|
||||
const deltaSeconds = delta / 1000;
|
||||
const maxRotation = PLAYER_ROTATION_SPEED * deltaSeconds;
|
||||
|
||||
// Calculate shortest angular difference
|
||||
const angleDiff = MathUtils.angleDifference(this.currentAngle, targetAngle);
|
||||
@ -244,28 +179,19 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
// Normalize current angle
|
||||
this.currentAngle = MathUtils.normalizeAngle(this.currentAngle);
|
||||
|
||||
// Acceleration logic: accelerate toward max speed with ease-out curve
|
||||
// This makes acceleration fast at first, then slows down as approaching max speed
|
||||
if (this.currentSpeed < maxSpeedMS) {
|
||||
const speedDiff = maxSpeedMS - this.currentSpeed;
|
||||
const accelerationFactor = speedDiff / maxSpeedMS; // 1.0 at start, 0.0 at max speed
|
||||
const dynamicAcceleration = PLAYER_ACCELERATION * (0.3 + accelerationFactor * 0.7); // Range: 30%-100% of base
|
||||
this.currentSpeed = Math.min(maxSpeedMS, this.currentSpeed + dynamicAcceleration * deltaSeconds);
|
||||
} else if (this.currentSpeed > maxSpeedMS) {
|
||||
// Decelerate if max speed was reduced (e.g., after goal)
|
||||
this.currentSpeed = Math.max(maxSpeedMS, this.currentSpeed - PLAYER_DECELERATION * deltaSeconds);
|
||||
}
|
||||
|
||||
// Update visual rotation
|
||||
// Update visual rotation (convert to degrees, subtract 90 because 0 angle is up in Phaser)
|
||||
this.setRotation(-this.currentAngle);
|
||||
|
||||
// Calculate velocity based on current facing direction and speed attribute
|
||||
// speed attribute (0-100) maps to actual m/s (e.g., 80 -> 8 m/s)
|
||||
const baseSpeed = (this.attributes.speed / SPEED_SCALE_FACTOR) * SCALE; // Convert to pixels/s
|
||||
const speed = baseSpeed * this.speedMultiplier; // Apply speed multiplier
|
||||
|
||||
// Move in the direction the player is currently facing
|
||||
const speedPixels = this.currentSpeed * SCALE; // Convert m/s to pixels/s
|
||||
const velX = Math.cos(this.currentAngle) * speedPixels;
|
||||
const velY = Math.sin(this.currentAngle) * speedPixels;
|
||||
const velX = Math.cos(this.currentAngle) * speed;
|
||||
const velY = Math.sin(this.currentAngle) * speed;
|
||||
|
||||
this.body.setVelocity(velX, -velY); // Negative Y because screen coords
|
||||
}
|
||||
|
||||
// Update game position based on physics body center
|
||||
const bodyX = this.body.x + this.body.width / 2;
|
||||
@ -273,59 +199,5 @@ export class Player extends Phaser.GameObjects.Container {
|
||||
const gamePos = CoordinateUtils.screenToGame(this.scene, bodyX, bodyY);
|
||||
this.gameX = gamePos.x;
|
||||
this.gameY = gamePos.y;
|
||||
|
||||
// Update debug visualizations
|
||||
this.updateDebugVisuals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can perform a tackle (cooldown expired)
|
||||
*/
|
||||
public canTackle(): boolean {
|
||||
const currentTime = Date.now();
|
||||
return (currentTime - this.lastTackleTime) >= TACKLE_COOLDOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that this player performed a tackle
|
||||
*/
|
||||
public setTacklePerformed() {
|
||||
this.lastTackleTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update debug visualizations (target position and path line)
|
||||
*/
|
||||
private updateDebugVisuals() {
|
||||
if (!DEBUG || !this.debugTargetGraphics || !this.debugLineGraphics) return;
|
||||
|
||||
// Convert target position to screen coordinates
|
||||
const targetScreen = CoordinateUtils.gameToScreen(this.scene, this.targetX, this.targetY);
|
||||
const playerScreen = CoordinateUtils.gameToScreen(this.scene, this.gameX, this.gameY);
|
||||
|
||||
// Clear previous debug graphics
|
||||
this.debugTargetGraphics.clear();
|
||||
this.debugLineGraphics.clear();
|
||||
|
||||
// Draw line from player to target
|
||||
const lineColor = this.team === 'home' ? 0x0000ff : 0xff0000;
|
||||
this.debugLineGraphics.lineStyle(1, lineColor, 0.5);
|
||||
this.debugLineGraphics.lineBetween(playerScreen.x, playerScreen.y, targetScreen.x, targetScreen.y);
|
||||
|
||||
// Draw target marker (X)
|
||||
const markerSize = 5;
|
||||
this.debugTargetGraphics.lineStyle(2, lineColor, 0.8);
|
||||
this.debugTargetGraphics.lineBetween(
|
||||
targetScreen.x - markerSize,
|
||||
targetScreen.y - markerSize,
|
||||
targetScreen.x + markerSize,
|
||||
targetScreen.y + markerSize
|
||||
);
|
||||
this.debugTargetGraphics.lineBetween(
|
||||
targetScreen.x + markerSize,
|
||||
targetScreen.y - markerSize,
|
||||
targetScreen.x - markerSize,
|
||||
targetScreen.y + markerSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,7 @@ import {
|
||||
MOVEMENT_STOP_THRESHOLD,
|
||||
PUCK_CARRY_DISTANCE,
|
||||
PUCK_PICKUP_RADIUS,
|
||||
SHOT_SPEED,
|
||||
TACKLE_SUCCESS_MODIFIER,
|
||||
TACKLE_PUCK_LOOSE_CHANCE
|
||||
SHOT_SPEED
|
||||
} from '../config/constants';
|
||||
import { Goal } from './Goal';
|
||||
import { Puck } from '../entities/Puck';
|
||||
@ -51,27 +49,11 @@ export class GameScene extends Phaser.Scene {
|
||||
'home',
|
||||
'C',
|
||||
-10,
|
||||
-5,
|
||||
{ speed: 80, skill: 75, tackling: 70, balance: 75 }
|
||||
-10,
|
||||
{ speed: 80, skill: 75 }
|
||||
);
|
||||
|
||||
// Create one away defender at (15, 0) - right side for defending
|
||||
const awayDefender = new Player(
|
||||
this,
|
||||
'away-LD',
|
||||
'away',
|
||||
'LD',
|
||||
15,
|
||||
0,
|
||||
{ speed: 75, skill: 70, tackling: 85, balance: 80 }
|
||||
);
|
||||
|
||||
this.players.push(homeCenter, awayDefender);
|
||||
|
||||
// Setup player-player collisions
|
||||
this.physics.add.collider(homeCenter, awayDefender, (obj1, obj2) => {
|
||||
this.handlePlayerCollision(obj1 as Player, obj2 as Player);
|
||||
});
|
||||
this.players.push(homeCenter);
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
@ -261,89 +243,4 @@ export class GameScene extends Phaser.Scene {
|
||||
this.puck.body.setVelocity(dirX * shotSpeed, -dirY * shotSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle collision between two players - determines and executes tackle
|
||||
*/
|
||||
private handlePlayerCollision(player1: Player, player2: Player) {
|
||||
// Determine who is the tackler and who is being tackled
|
||||
let tackler: Player;
|
||||
let tackled: Player;
|
||||
|
||||
// If one player has the puck, the other tackles
|
||||
if (this.puck.carrier === player1.id) {
|
||||
tackler = player2;
|
||||
tackled = player1;
|
||||
} else if (this.puck.carrier === player2.id) {
|
||||
tackler = player1;
|
||||
tackled = player2;
|
||||
} else {
|
||||
// Neither has puck - faster player tackles slower one
|
||||
const player1Speed = Math.sqrt(player1.body.velocity.x ** 2 + player1.body.velocity.y ** 2);
|
||||
const player2Speed = Math.sqrt(player2.body.velocity.x ** 2 + player2.body.velocity.y ** 2);
|
||||
|
||||
if (player1Speed >= player2Speed) {
|
||||
tackler = player1;
|
||||
tackled = player2;
|
||||
} else {
|
||||
tackler = player2;
|
||||
tackled = player1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if EITHER player is on cooldown (prevents double tackle execution)
|
||||
if (!tackler.canTackle() || !tackled.canTackle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute tackle and mark both players as having participated
|
||||
this.executeTackle(tackler, tackled);
|
||||
tackled.setTacklePerformed(); // Mark tackled player too to prevent instant counter-tackle
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute tackle using tackling skill vs balance skill
|
||||
*/
|
||||
private executeTackle(tackler: Player, tackled: Player) {
|
||||
// Calculate tackle success based on tackling vs balance
|
||||
// Formula: (tackling / (tackling + balance)) * modifier
|
||||
const tacklerSkill = tackler.attributes.tackling;
|
||||
const tackledBalance = tackled.attributes.balance;
|
||||
|
||||
const successChance = (tacklerSkill / (tacklerSkill + tackledBalance)) * TACKLE_SUCCESS_MODIFIER;
|
||||
const success = Math.random() < successChance;
|
||||
|
||||
console.log(
|
||||
`[Tackle] ${tackler.id} (tackling: ${tacklerSkill}) tackles ${tackled.id} (balance: ${tackledBalance}) - ${success ? 'SUCCESS' : 'FAILED'} (${(successChance * 100).toFixed(1)}%)`
|
||||
);
|
||||
|
||||
// Mark tackle performed (cooldown starts)
|
||||
tackler.setTacklePerformed();
|
||||
|
||||
if (success) {
|
||||
// Successful tackle
|
||||
if (this.puck.carrier === tackled.id) {
|
||||
// Tackled player had puck - determine if it becomes loose
|
||||
if (Math.random() < TACKLE_PUCK_LOOSE_CHANCE) {
|
||||
console.log(`[Tackle] Puck knocked loose!`);
|
||||
this.puck.setLoose();
|
||||
|
||||
// Give puck some velocity in random direction
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 3 * SCALE;
|
||||
this.puck.body.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
|
||||
} else {
|
||||
// Tackler takes possession
|
||||
console.log(`[Tackle] ${tackler.id} steals the puck!`);
|
||||
this.puck.setCarrier(tackler.id, tackler.team);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply physical impact to tackled player (significantly slow them down)
|
||||
tackled.body.setVelocity(
|
||||
tackled.body.velocity.x * 0.1,
|
||||
tackled.body.velocity.y * 0.1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,20 +27,17 @@ export class DefensiveBehavior extends BehaviorNode {
|
||||
if (puck.state !== 'carried') return false;
|
||||
if (puck.carrier === player.id) return false;
|
||||
|
||||
// Check if carrier is opponent (different team)
|
||||
return puck.carrierTeam !== player.team;
|
||||
// TODO: Check if carrier is opponent (requires team roster access)
|
||||
// For now, just fail - will implement in Phase 6
|
||||
return false;
|
||||
}),
|
||||
|
||||
// Chase the puck carrier
|
||||
new Action((player, gameState) => {
|
||||
const { puck } = gameState;
|
||||
|
||||
return {
|
||||
type: 'chase_puck',
|
||||
targetX: puck.gameX,
|
||||
targetY: puck.gameY
|
||||
};
|
||||
})
|
||||
// Defensive action (future implementation)
|
||||
new Action((player) => ({
|
||||
type: 'idle',
|
||||
targetX: player.gameX,
|
||||
targetY: player.gameY
|
||||
}))
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -27,8 +27,6 @@ export type PuckState = 'loose' | 'carried' | 'passing' | 'shot';
|
||||
export interface PlayerAttributes {
|
||||
speed: number; // 0-100: movement speed
|
||||
skill: number; // 0-100: pass/shot accuracy, decision quality
|
||||
tackling: number; // 0-100: ability to execute successful tackles
|
||||
balance: number; // 0-100: ability to resist being tackled
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user