Phase 2: ECS foundation — world, components, systems, bridge

- bitECS world with time tracking (delta, elapsed, tick)
- 5 components: Position, Velocity, SpriteRef, Health, ChemicalComposition
- Movement system (velocity * delta) + bounce system (boundary reflection)
- Health system with damage, healing, death detection
- Entity factory (createGameEntity/removeGameEntity)
- Phaser bridge: polling sync creates/destroys/updates circle sprites
- GameScene: 20 colored circles bouncing at 60fps
- BootScene: click-to-start transition, version bump to v0.2.0
- 39 ECS unit tests passing (74 total)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Денис Шкабатур
2026-02-12 12:34:06 +03:00
parent 58ebb11747
commit ddbca12740
11 changed files with 1042 additions and 18 deletions

91
src/scenes/GameScene.ts Normal file
View File

@@ -0,0 +1,91 @@
import Phaser from 'phaser';
import { createGameWorld, updateTime, type GameWorld } from '../ecs/world';
import { movementSystem, bounceSystem } from '../ecs/systems/movement';
import { healthSystem } from '../ecs/systems/health';
import { createGameEntity, removeGameEntity } from '../ecs/factory';
import { PhaserBridge } from '../ecs/bridge';
import { GAME_WIDTH, GAME_HEIGHT } from '../config';
const ENTITY_COUNT = 20;
const COLORS = [
0x00ff88, 0xff0044, 0x44aaff, 0xffaa00,
0xff44ff, 0x44ffaa, 0xffff44, 0xaa44ff,
];
export class GameScene extends Phaser.Scene {
private gameWorld!: GameWorld;
private bridge!: PhaserBridge;
private statsText!: Phaser.GameObjects.Text;
constructor() {
super({ key: 'GameScene' });
}
create(): void {
// 1. Initialize ECS
this.gameWorld = createGameWorld();
this.bridge = new PhaserBridge(this);
// 2. Spawn bouncing circles with random properties
for (let i = 0; i < ENTITY_COUNT; i++) {
const speed = 50 + Math.random() * 150;
const angle = Math.random() * Math.PI * 2;
createGameEntity(this.gameWorld.world, {
position: {
x: 100 + Math.random() * (GAME_WIDTH - 200),
y: 100 + Math.random() * (GAME_HEIGHT - 200),
},
velocity: {
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
},
sprite: {
color: COLORS[i % COLORS.length],
radius: 6 + Math.random() * 14,
},
health: {
current: 100,
max: 100,
},
});
}
// 3. UI labels
this.add.text(10, 10, 'Phase 2: ECS Foundation', {
fontSize: '14px',
color: '#00ff88',
fontFamily: 'monospace',
});
this.statsText = this.add.text(10, 30, '', {
fontSize: '12px',
color: '#557755',
fontFamily: 'monospace',
});
}
update(_time: number, delta: number): void {
// 1. Update world time
updateTime(this.gameWorld, delta);
// 2. Run systems
movementSystem(this.gameWorld.world, delta);
bounceSystem(this.gameWorld.world, GAME_WIDTH, GAME_HEIGHT);
// 3. Health check + cleanup
const dead = healthSystem(this.gameWorld.world);
for (const eid of dead) {
removeGameEntity(this.gameWorld.world, eid);
}
// 4. Sync to Phaser
this.bridge.sync(this.gameWorld.world);
// 5. Update stats
const fps = delta > 0 ? Math.round(1000 / delta) : 0;
this.statsText.setText(
`${this.bridge.entityCount} entities | tick ${this.gameWorld.time.tick} | ${fps} fps`,
);
}
}