puck 1
This commit is contained in:
parent
92b2d30d2b
commit
1990d98aa2
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A hockey match engine simulation built with PhaserJS and TypeScript. The engine uses continuous positioning (exact X/Y coordinates on a 2D rink) combined with behavior trees for AI decision-making to create a realistic hockey match simulation.
|
||||||
|
|
||||||
|
**Core Concept**: Players have exact positions on a 60x30m international hockey rink, with AI running behavior trees at 60 FPS to make tactical decisions (passing, shooting, positioning, etc.).
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start development server (runs on port 3000)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Preview production build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
**Critical**: The rink uses a centered coordinate system:
|
||||||
|
- Origin (0, 0) = center of rink
|
||||||
|
- Rink dimensions: 60m length × 30m width
|
||||||
|
- Left goal: x = -26m, Right goal: x = +26m
|
||||||
|
- All positions use **meters as floats** (not integers)
|
||||||
|
- Screen rendering: 1 meter = 20 pixels (SCALE constant)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
config/
|
||||||
|
constants.ts # All game constants (rink dimensions, colors, speeds)
|
||||||
|
game/
|
||||||
|
main.ts # Phaser game initialization and config
|
||||||
|
GameScene.ts # Main game scene (rendering, game loop)
|
||||||
|
Goal.ts # Goal structure with physics bodies
|
||||||
|
entities/ # Player and Puck classes (planned)
|
||||||
|
systems/ # BehaviorTree, PositioningSystem, PuckSystem (planned)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Phases (from PLAN.md)
|
||||||
|
|
||||||
|
The project follows a phased approach:
|
||||||
|
1. **Phase 1** (✓): Environment setup, rink rendering
|
||||||
|
2. **Phase 2** (Next): Player entities (12 players: 10 skaters + 2 goalies) with smooth movement
|
||||||
|
3. **Phase 3**: Puck entity and possession mechanics
|
||||||
|
4. **Phase 4**: Behavior tree for puck carrier decisions (shoot/pass/carry)
|
||||||
|
5. **Phase 5**: Team offensive positioning system
|
||||||
|
6. **Phase 6**: Defensive behaviors and pressure mechanics
|
||||||
|
7. **Phase 7**: Goalie AI
|
||||||
|
8. **Phase 8**: Game flow (goals, faceoffs, scoring)
|
||||||
|
9. **Phase 9**: Polish and tuning
|
||||||
|
|
||||||
|
### Key Systems (Planned)
|
||||||
|
|
||||||
|
**Behavior Trees**: Decision-making engine that runs every tick
|
||||||
|
- Evaluates game state (possession, positions, threats)
|
||||||
|
- Weights actions by player attributes (Hockey IQ, Skill, Speed)
|
||||||
|
- Outputs: move, pass, shoot, or defensive actions
|
||||||
|
|
||||||
|
**Tactical Positioning**: Heat map-based positioning
|
||||||
|
- Different formations based on game situation (offense/defense)
|
||||||
|
- Players move toward ideal positions modified by:
|
||||||
|
- Puck location
|
||||||
|
- Teammate spacing
|
||||||
|
- Opponent positions
|
||||||
|
- Player stamina/speed
|
||||||
|
|
||||||
|
**Puck Movement**:
|
||||||
|
- Pass success = f(passer skill, distance, pressure, receiver skill)
|
||||||
|
- Shots use trajectory calculation with goalie save probability
|
||||||
|
- Smooth interpolation for visual feedback
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- **Framework**: Phaser 3.90.0 (with Arcade Physics)
|
||||||
|
- **TypeScript**: Strict mode enabled
|
||||||
|
- **Build Tool**: Vite 5.4
|
||||||
|
- **Target FPS**: 60 (constant in constants.ts)
|
||||||
|
- **Physics**: Arcade physics with zero gravity (top-down view)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All magic numbers and game constants are centralized in `src/config/constants.ts`:
|
||||||
|
- Rink dimensions and zone lines
|
||||||
|
- Goal dimensions
|
||||||
|
- Colors (ice, boards, lines)
|
||||||
|
- Scale factor (meters to pixels)
|
||||||
|
- Game settings (FPS)
|
||||||
|
|
||||||
|
Future constants will include:
|
||||||
|
- Player speeds, shot speeds, pass speeds
|
||||||
|
- AI decision probabilities
|
||||||
|
- Attribute ranges
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- **Incremental testing**: Test each phase thoroughly before proceeding
|
||||||
|
- **Debug visualization**: Add toggleable overlays for targets, zones, etc.
|
||||||
|
- **Tuning over precision**: Numbers are starting points; tune based on feel
|
||||||
|
- Refer to PLAN.md for detailed phase requirements and validation criteria
|
||||||
|
- Refer to NOTES.md for architectural decisions and design notes
|
||||||
112
src/entities/Puck.ts
Normal file
112
src/entities/Puck.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import { SCALE } from '../config/constants';
|
||||||
|
|
||||||
|
export type PuckState = 'loose' | 'carried' | 'passing' | 'shot';
|
||||||
|
|
||||||
|
export class Puck extends Phaser.GameObjects.Container {
|
||||||
|
private sprite!: Phaser.GameObjects.Graphics;
|
||||||
|
private body!: Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Position in game coordinates (meters)
|
||||||
|
public gameX: number;
|
||||||
|
public gameY: number;
|
||||||
|
|
||||||
|
// Puck state
|
||||||
|
public state: PuckState;
|
||||||
|
public possession: 'home' | 'away' | null;
|
||||||
|
public carrier: string | null; // PlayerID
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, gameX: number = 0, gameY: number = 0) {
|
||||||
|
// 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.gameX = gameX;
|
||||||
|
this.gameY = gameY;
|
||||||
|
this.state = 'loose';
|
||||||
|
this.possession = null;
|
||||||
|
this.carrier = null;
|
||||||
|
|
||||||
|
// Add to scene
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
this.body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Set physics body size to match the puck visual (10px diameter)
|
||||||
|
this.body.setSize(10, 10);
|
||||||
|
this.body.setOffset(-5, -5); // Center the body on the container
|
||||||
|
|
||||||
|
// Set initial velocity (5 m/s in a random direction for testing)
|
||||||
|
const angle = 3.1415; //Math.random() * Math.PI * 2;
|
||||||
|
const speed = 30 * 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
|
||||||
|
this.body.setBounce(1, 1);
|
||||||
|
this.body.setCollideWorldBounds(true);
|
||||||
|
|
||||||
|
this.createSprite();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSprite() {
|
||||||
|
const graphics = this.scene.add.graphics();
|
||||||
|
|
||||||
|
// Draw puck as black circle (5px radius)
|
||||||
|
graphics.fillStyle(0x000000, 1);
|
||||||
|
graphics.fillCircle(0, 0, 5);
|
||||||
|
|
||||||
|
// Add white highlight for visibility on ice
|
||||||
|
graphics.fillStyle(0xffffff, 0.3);
|
||||||
|
graphics.fillCircle(-1, -1, 2);
|
||||||
|
|
||||||
|
this.add(graphics);
|
||||||
|
this.sprite = graphics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update puck 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 puck to be carried by a player
|
||||||
|
*/
|
||||||
|
public setCarrier(playerId: string, team: 'home' | 'away') {
|
||||||
|
this.carrier = playerId;
|
||||||
|
this.possession = team;
|
||||||
|
this.state = 'carried';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set puck to loose state
|
||||||
|
*/
|
||||||
|
public setLoose() {
|
||||||
|
this.carrier = null;
|
||||||
|
this.state = 'loose';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get puck position in game coordinates
|
||||||
|
*/
|
||||||
|
public getGamePosition(): { x: number; y: number } {
|
||||||
|
return { x: this.gameX, y: this.gameY };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,12 @@ import {
|
|||||||
COLOR_GOAL_CREASE
|
COLOR_GOAL_CREASE
|
||||||
} from '../config/constants';
|
} from '../config/constants';
|
||||||
import { Goal } from './Goal';
|
import { Goal } from './Goal';
|
||||||
|
import { Puck } from '../entities/Puck';
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'GameScene' });
|
super({ key: 'GameScene' });
|
||||||
@ -27,6 +29,20 @@ export class GameScene extends Phaser.Scene {
|
|||||||
create() {
|
create() {
|
||||||
this.drawRink();
|
this.drawRink();
|
||||||
this.createGoals();
|
this.createGoals();
|
||||||
|
this.createPuck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPuck() {
|
||||||
|
// Initialize puck at center ice (0, 0 in game coordinates)
|
||||||
|
this.puck = new Puck(this, 0, 0);
|
||||||
|
|
||||||
|
// Add collisions between puck and goal posts
|
||||||
|
this.physics.add.collider(this.puck, this.leftGoal.getLeftPost());
|
||||||
|
this.physics.add.collider(this.puck, this.leftGoal.getRightPost());
|
||||||
|
this.physics.add.collider(this.puck, this.leftGoal.getBackBar());
|
||||||
|
this.physics.add.collider(this.puck, this.rightGoal.getLeftPost());
|
||||||
|
this.physics.add.collider(this.puck, this.rightGoal.getRightPost());
|
||||||
|
this.physics.add.collider(this.puck, this.rightGoal.getBackBar());
|
||||||
}
|
}
|
||||||
|
|
||||||
private createGoals() {
|
private createGoals() {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
default: 'arcade',
|
default: 'arcade',
|
||||||
arcade: {
|
arcade: {
|
||||||
gravity: { x: 0, y: 0 },
|
gravity: { x: 0, y: 0 },
|
||||||
debug: false
|
debug: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fps: {
|
fps: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user