Files
chicken-game/docs/ARCHITECTURE.md
vigdorov e07aaac979
All checks were successful
continuous-integration/drone/push Build is passing
fix
2026-01-17 11:53:17 +03:00

945 lines
25 KiB
Markdown

# Architecture
Technical architecture for the Roguelite Platformer game.
---
## Project Structure
```
src/
├── main.ts # Entry point, Phaser config
├── types/ # TypeScript interfaces
│ ├── index.ts # Re-exports
│ ├── game-state.ts # RunState, MetaState
│ ├── entities.ts # EnemyConfig, HazardConfig
│ ├── upgrades.ts # Upgrade, UpgradeEffect
│ └── level-gen.ts # RoomTemplate, SpawnPoint
├── config/ # Configuration
│ ├── game.config.ts # Phaser config
│ ├── physics.config.ts # Arcade physics settings
│ ├── controls.config.ts # Key bindings
│ ├── balance.config.ts # Balance (speeds, timings)
│ └── upgrades.config.ts # All shop upgrades
├── scenes/ # Phaser scenes
│ ├── BootScene.ts # Initialization
│ ├── PreloadScene.ts # Asset loading
│ ├── MenuScene.ts # Main menu
│ ├── GameScene.ts # Core gameplay
│ ├── UIScene.ts # HUD (parallel with GameScene)
│ ├── PauseScene.ts # Pause menu
│ ├── ShopScene.ts # Upgrade shop
│ └── GameOverScene.ts # Death screen
├── entities/ # Game objects
│ ├── Player.ts # Player entity
│ ├── PlayerController.ts # Controls + game feel
│ ├── enemies/ # Enemy types
│ │ ├── Enemy.ts # Base class
│ │ ├── Patroller.ts # Patrol enemy
│ │ ├── Jumper.ts # Jumping enemy
│ │ ├── Flyer.ts # Flying enemy
│ │ ├── Chaser.ts # Chasing enemy
│ │ └── Sprinter.ts # Fast patrol enemy
│ ├── Projectile.ts # Player projectile
│ ├── Coin.ts # Collectible coin
│ ├── PowerUp.ts # Power-up item
│ └── hazards/ # Hazard types
│ ├── Spikes.ts # Static spikes
│ ├── FallingPlatform.ts # Crumbling platform
│ ├── Saw.ts # Moving saw
│ ├── Turret.ts # Shooting turret
│ └── Laser.ts # Toggle laser
├── systems/ # Managers/Systems
│ ├── InputManager.ts # Input buffering, abstraction
│ ├── AudioManager.ts # Sound and music
│ ├── SaveManager.ts # localStorage persistence
│ ├── GameStateManager.ts # Run state + Meta state
│ ├── UpgradeManager.ts # Upgrade application
│ ├── ScoreManager.ts # Score and combo
│ ├── ParticleManager.ts # Particle effects
│ └── CameraManager.ts # Screen shake
├── level-generation/ # Procedural generation
│ ├── LevelGenerator.ts # Main generator
│ ├── RoomPlacer.ts # Room placement
│ ├── PathValidator.ts # Path validation
│ └── templates/ # JSON room templates
├── ui/ # UI components
│ ├── HUD.ts # Heads-up display
│ ├── Button.ts # Reusable button
│ ├── ProgressBar.ts # Progress/health bar
│ └── ComboDisplay.ts # Combo counter
└── utils/ # Utilities
├── math.ts # Math helpers
└── SeededRandom.ts # Seed-based RNG
```
---
## Scene Architecture
### Scene Flow
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ BootScene → PreloadScene → MenuScene ─┬→ GameScene ←──┐ │
│ │ │ │ │
│ │ ▼ │ │
│ │ UIScene │ │
│ │ (parallel) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ PauseScene │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ GameOverScene│ │
│ │ │ │ │
│ │ ▼ │ │
│ └→ ShopScene ───┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Scene Responsibilities
| Scene | Responsibility |
|-------|---------------|
| **BootScene** | Initialize game settings, configure physics |
| **PreloadScene** | Load all assets (sprites, audio, tilemaps) |
| **MenuScene** | Main menu, navigation to other scenes |
| **GameScene** | Core gameplay loop, entity management |
| **UIScene** | HUD overlay, runs parallel with GameScene |
| **PauseScene** | Pause menu, settings access during gameplay |
| **ShopScene** | Meta-progression shop, upgrade purchases |
| **GameOverScene** | Death screen, stats, navigation |
### Scene Communication
Scenes communicate through:
1. **Registry** (`this.registry`) - shared data store
2. **Events** (`this.events`) - event-based messaging
3. **Scene Manager** - scene transitions
```typescript
// Example: GameScene emits coin collection
this.events.emit('coin-collected', { amount: 1, total: this.coins });
// UIScene listens and updates display
this.scene.get('GameScene').events.on('coin-collected', this.updateCoinDisplay, this);
```
---
## State Management
### Dual-State Pattern
The game uses two separate state objects:
```typescript
// RunState - reset every run
interface RunState {
currentLevel: number;
coins: number;
score: number;
combo: number;
projectilesAvailable: number;
activePowerUp: PowerUpType | null;
powerUpTimer: number;
hp: number;
hasUsedSecondChance: boolean;
enemiesStunned: number;
timeElapsed: number;
levelSeed: string;
}
// MetaState - persists across runs
interface MetaState {
gasCoins: number;
purchasedUpgrades: string[];
activeUpgrades: string[];
achievements: string[];
totalCoinsCollected: number;
totalEnemiesStunned: number;
totalDeaths: number;
highScores: HighScore[];
settings: GameSettings;
}
```
### State Flow
```
┌────────────────────────────────────────────────────────────┐
│ GameStateManager │
├────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ RunState │ │ MetaState │ │
│ │ (memory) │ │ (localStorage) │ │
│ ├─────────────┤ ├─────────────────────────┤ │
│ │ level: 1 │ ──death──▶ │ gasCoins += runCoins │ │
│ │ coins: 42 │ │ totalDeaths++ │ │
│ │ score: 1200 │ │ highScores.push(...) │ │
│ │ ... │ │ │ │
│ └─────────────┘ └─────────────────────────┘ │
│ │ │ │
│ │ reset on new run │ load on boot │
│ ▼ ▼ save on change │
│ │
└────────────────────────────────────────────────────────────┘
```
---
## Entity System
### Entity Hierarchy
```
Phaser.GameObjects.Sprite
┌───────────────┼───────────────┐
│ │ │
Player Enemy Hazard
│ │ │
│ ┌───────┼───────┐ │
│ │ │ │ │
│ Patroller Flyer Chaser │
│ │ │ │
│ Jumper Sprinter │
│ │
│ ┌───────┼───────┐
│ │ │ │
│ Spikes Saw Laser
│ │
│ FallingPlatform
│ │
│ Turret
PlayerController
```
### Enemy States
```typescript
enum EnemyState {
ACTIVE = 'active', // Dangerous, kills on contact
STUNNED = 'stunned', // Safe, can pass through
RECOVERING = 'recovering' // Brief transition state
}
```
State machine for enemies:
```
┌─────────┐ hit by projectile ┌─────────┐ timer expires ┌────────────┐
│ ACTIVE │ ─────────────────▶ │ STUNNED │ ──────────────▶ │ RECOVERING │
└─────────┘ └─────────┘ └────────────┘
▲ │
│ recovery complete │
└────────────────────────────────────────────────────────────┘
```
---
## Physics Configuration
### Arcade Physics Settings
```typescript
// physics.config.ts
export const PHYSICS_CONFIG = {
gravity: { y: 1200 },
player: {
speed: 300,
jumpVelocity: -500,
acceleration: 2000,
drag: 1500,
},
projectile: {
speed: 600,
lifetime: 1000, // ms
},
enemies: {
patroller: { speed: 100 },
jumper: { speed: 80, jumpForce: -400 },
flyer: { speed: 120 },
chaser: { speed: 60 },
sprinter: { speed: 200 },
},
};
```
### Collision Groups
```
Player ←→ Enemy (active) = Death
Player ←→ Enemy (stunned) = Pass through
Player ←→ Hazard = Death
Player ←→ Coin = Collect
Player ←→ PowerUp = Collect
Player ←→ Platform = Land
Projectile ←→ Enemy = Stun enemy
Projectile ←→ Wall = Destroy (or ricochet)
```
---
## Game Feel Implementation
### Coyote Time
```typescript
// PlayerController.ts
private coyoteTime = 100; // ms
private coyoteTimer = 0;
private wasGrounded = false;
update(delta: number) {
if (this.body.onFloor()) {
this.coyoteTimer = this.coyoteTime;
this.wasGrounded = true;
} else if (this.wasGrounded) {
this.coyoteTimer -= delta;
if (this.coyoteTimer <= 0) {
this.wasGrounded = false;
}
}
}
canJump(): boolean {
return this.body.onFloor() || this.coyoteTimer > 0;
}
```
### Input Buffering
```typescript
// InputManager.ts
private jumpBufferTime = 100; // ms
private jumpBufferTimer = 0;
update(delta: number) {
if (this.jumpPressed) {
this.jumpBufferTimer = this.jumpBufferTime;
} else {
this.jumpBufferTimer = Math.max(0, this.jumpBufferTimer - delta);
}
}
hasBufferedJump(): boolean {
return this.jumpBufferTimer > 0;
}
consumeJumpBuffer() {
this.jumpBufferTimer = 0;
}
```
### Variable Jump Height
```typescript
// PlayerController.ts
private minJumpVelocity = -250;
private maxJumpVelocity = -500;
onJumpPressed() {
if (this.canJump()) {
this.body.setVelocityY(this.maxJumpVelocity);
this.isJumping = true;
}
}
onJumpReleased() {
if (this.isJumping && this.body.velocity.y < this.minJumpVelocity) {
this.body.setVelocityY(this.minJumpVelocity);
}
this.isJumping = false;
}
```
### Edge Correction
```typescript
// PlayerController.ts
private edgeCorrectionDistance = 4; // pixels
handleCollision(tile: Phaser.Tilemaps.Tile) {
if (this.body.velocity.y < 0) { // Moving upward
const overlap = this.getHorizontalOverlap(tile);
if (overlap > 0 && overlap <= this.edgeCorrectionDistance) {
this.x += overlap * this.getOverlapDirection(tile);
}
}
}
```
---
## Level Generation
### Spelunky-Style Room Grid
```
┌─────┬─────┬─────┬─────┐
│ S │ │ │ │ S = Start
├─────┼─────┼─────┼─────┤ E = Exit
│ ↓ │ ←──┤ │ │ ↓ → = Path direction
├─────┼─────┼─────┼─────┤
│ │ ↓ │ │ │
├─────┼─────┼─────┼─────┤
│ │ E │ │ │
└─────┴─────┴─────┴─────┘
```
### Generation Algorithm
```typescript
// LevelGenerator.ts
class LevelGenerator {
generateLevel(level: number, seed: string): Level {
const rng = new SeededRandom(seed);
const config = this.getLevelConfig(level);
// 1. Create room grid
const grid = this.createGrid(config.gridSize);
// 2. Generate guaranteed path
const path = this.generatePath(grid, rng);
// 3. Place room templates along path
this.placeRooms(grid, path, config.difficulty, rng);
// 4. Fill remaining cells with optional rooms
this.fillOptionalRooms(grid, rng);
// 5. Place entities
this.placeCoins(grid, config.coinCount, rng);
this.placeEnemies(grid, config.enemyCount, level, rng);
this.placeHazards(grid, config.hazardCount, level, rng);
this.placePowerUp(grid, rng); // 10% chance
// 6. Validate
if (!this.validatePath(grid)) {
return this.generateLevel(level, seed + '_retry');
}
return this.buildLevel(grid);
}
}
```
### Room Templates
```typescript
// types/level-gen.ts
interface RoomTemplate {
id: string;
width: number; // tiles
height: number; // tiles
difficulty: number; // 1-10
exits: {
top: boolean;
bottom: boolean;
left: boolean;
right: boolean;
};
tiles: number[][]; // tile IDs
spawnPoints: SpawnPoint[];
}
interface SpawnPoint {
type: 'coin' | 'enemy' | 'hazard' | 'powerup';
x: number;
y: number;
probability?: number;
}
```
---
## Upgrade System
### Upgrade Configuration
```typescript
// config/upgrades.config.ts
export const UPGRADES: Upgrade[] = [
{
id: 'light_boots',
name: 'Light Boots',
description: '+10% movement speed',
category: 'character',
price: 100,
effects: [{ type: 'speed_multiplier', value: 1.1 }],
unlockRequirement: null,
},
{
id: 'heavy_armor',
name: 'Heavy Armor',
description: '+1 HP per run, -15% speed',
category: 'character',
price: 500,
effects: [
{ type: 'extra_hp', value: 1 },
{ type: 'speed_multiplier', value: 0.85 },
],
unlockRequirement: null,
},
// ... more upgrades
];
```
### Upgrade Application
```typescript
// UpgradeManager.ts
class UpgradeManager {
applyUpgrades(player: Player, activeUpgrades: string[]) {
const stats = { ...DEFAULT_PLAYER_STATS };
for (const upgradeId of activeUpgrades) {
const upgrade = this.getUpgrade(upgradeId);
for (const effect of upgrade.effects) {
this.applyEffect(stats, effect);
}
}
player.setStats(stats);
}
private applyEffect(stats: PlayerStats, effect: UpgradeEffect) {
switch (effect.type) {
case 'speed_multiplier':
stats.speed *= effect.value;
break;
case 'jump_multiplier':
stats.jumpVelocity *= effect.value;
break;
case 'extra_hp':
stats.maxHp += effect.value;
break;
case 'extra_projectile':
stats.maxProjectiles += effect.value;
break;
// ... more effect types
}
}
}
```
---
## Audio System
### Audio Manager
```typescript
// AudioManager.ts
class AudioManager {
private music: Phaser.Sound.BaseSound | null = null;
private sfxVolume: number = 1;
private musicVolume: number = 0.5;
playMusic(key: string, loop = true) {
if (this.music) this.music.stop();
this.music = this.scene.sound.add(key, {
loop,
volume: this.musicVolume
});
this.music.play();
}
playSFX(key: string, config?: Phaser.Types.Sound.SoundConfig) {
this.scene.sound.play(key, {
volume: this.sfxVolume,
...config
});
}
setMusicVolume(volume: number) {
this.musicVolume = volume;
if (this.music) this.music.setVolume(volume);
}
setSFXVolume(volume: number) {
this.sfxVolume = volume;
}
}
```
### Sound Effects List
| Event | Sound Key |
|-------|-----------|
| Jump | `sfx_jump` |
| Land | `sfx_land` |
| Shoot | `sfx_shoot` |
| Enemy stunned | `sfx_stun` |
| Coin collected | `sfx_coin` |
| PowerUp collected | `sfx_powerup` |
| Player death | `sfx_death` |
| Level complete | `sfx_level_complete` |
| Menu select | `sfx_menu_select` |
| Purchase | `sfx_purchase` |
---
## Save System
### SaveManager
```typescript
// SaveManager.ts
class SaveManager {
private readonly SAVE_KEY = 'roguelite_platformer_save';
save(metaState: MetaState): void {
const data = JSON.stringify(metaState);
localStorage.setItem(this.SAVE_KEY, data);
}
load(): MetaState | null {
const data = localStorage.getItem(this.SAVE_KEY);
if (!data) return null;
try {
return JSON.parse(data) as MetaState;
} catch {
console.error('Failed to parse save data');
return null;
}
}
reset(): void {
localStorage.removeItem(this.SAVE_KEY);
}
exists(): boolean {
return localStorage.getItem(this.SAVE_KEY) !== null;
}
}
```
### Data Stored
```typescript
interface SaveData {
version: string; // For migration
metaState: MetaState;
lastPlayed: number; // Timestamp
}
```
---
## TypeScript Interfaces
### Core Types
```typescript
// types/game-state.ts
interface RunState {
currentLevel: number;
coins: number;
totalCoinsOnLevel: number;
score: number;
combo: number;
comboTimer: number;
projectilesAvailable: number;
projectileCooldowns: number[];
activePowerUp: PowerUpType | null;
powerUpTimer: number;
hp: number;
maxHp: number;
hasUsedSecondChance: boolean;
enemiesStunned: number;
timeElapsed: number;
levelSeed: string;
isPaused: boolean;
}
interface MetaState {
gasCoins: number;
purchasedUpgrades: string[];
activeUpgrades: string[];
achievements: Achievement[];
stats: GlobalStats;
highScores: HighScore[];
settings: GameSettings;
}
interface GlobalStats {
totalCoinsCollected: number;
totalEnemiesStunned: number;
totalDeaths: number;
totalLevelsCompleted: number;
totalPlayTime: number;
highestLevel: number;
highestCombo: number;
}
interface HighScore {
score: number;
level: number;
date: number;
seed?: string;
}
interface GameSettings {
musicVolume: number;
sfxVolume: number;
screenShake: boolean;
showFPS: boolean;
}
```
### Entity Types
```typescript
// types/entities.ts
interface EnemyConfig {
type: EnemyType;
speed: number;
patrolDistance?: number;
jumpInterval?: number;
flightPattern?: FlightPattern;
detectionRange?: number;
}
type EnemyType = 'patroller' | 'jumper' | 'flyer' | 'chaser' | 'sprinter';
interface HazardConfig {
type: HazardType;
damage: number;
interval?: number; // For turrets, lasers
path?: Phaser.Math.Vector2[]; // For saws
}
type HazardType = 'spikes' | 'falling_platform' | 'saw' | 'turret' | 'laser';
type PowerUpType = 'shield' | 'magnet' | 'clock' | 'coffee' | 'infinity' | 'ghost';
interface PowerUpConfig {
type: PowerUpType;
duration: number;
effect: () => void;
}
```
### Upgrade Types
```typescript
// types/upgrades.ts
interface Upgrade {
id: string;
name: string;
description: string;
category: UpgradeCategory;
price: number;
effects: UpgradeEffect[];
unlockRequirement: UnlockRequirement | null;
tradeoff?: string; // Description of downside
}
type UpgradeCategory =
| 'character'
| 'weapon'
| 'survival'
| 'risk_reward'
| 'cosmetic';
interface UpgradeEffect {
type: EffectType;
value: number;
}
type EffectType =
| 'speed_multiplier'
| 'jump_multiplier'
| 'extra_hp'
| 'extra_projectile'
| 'projectile_cooldown_multiplier'
| 'projectile_range_multiplier'
| 'stun_duration_multiplier'
| 'coin_multiplier'
| 'score_multiplier'
| 'hitbox_multiplier'
| 'enable_double_jump'
| 'enable_dash'
| 'enable_ricochet'
| 'enable_explosive';
interface UnlockRequirement {
type: 'level_reached' | 'coins_collected' | 'deaths' | 'enemies_stunned' | 'achievement';
value: number | string;
}
```
---
## Visual Effects
### Particle System
```typescript
// ParticleManager.ts
class ParticleManager {
private emitters: Map<string, Phaser.GameObjects.Particles.ParticleEmitter>;
createDustEffect(x: number, y: number) {
this.emitters.get('dust')?.explode(5, x, y);
}
createStunEffect(x: number, y: number) {
this.emitters.get('stars')?.explode(8, x, y);
}
createCoinSparkle(x: number, y: number) {
this.emitters.get('sparkle')?.explode(3, x, y);
}
createDeathEffect(x: number, y: number) {
this.emitters.get('explosion')?.explode(20, x, y);
}
}
```
### Screen Shake
```typescript
// CameraManager.ts
class CameraManager {
private camera: Phaser.Cameras.Scene2D.Camera;
private shakeEnabled: boolean = true;
shake(intensity: number, duration: number) {
if (!this.shakeEnabled) return;
this.camera.shake(duration, intensity);
}
microShake() {
this.shake(0.002, 50);
}
deathShake() {
this.shake(0.01, 200);
}
setShakeEnabled(enabled: boolean) {
this.shakeEnabled = enabled;
}
}
```
### Squash & Stretch
```typescript
// Player.ts
jumpSquash() {
this.setScale(0.8, 1.2);
this.scene.tweens.add({
targets: this,
scaleX: 1,
scaleY: 1,
duration: 150,
ease: 'Back.out',
});
}
landSquash() {
this.setScale(1.2, 0.8);
this.scene.tweens.add({
targets: this,
scaleX: 1,
scaleY: 1,
duration: 100,
ease: 'Back.out',
});
}
```
### Hitstop
```typescript
// GameScene.ts
hitStop(duration: number = 50) {
this.physics.pause();
this.time.delayedCall(duration, () => {
this.physics.resume();
});
}
```
---
## Performance Considerations
### Object Pooling
```typescript
// Projectile pooling
class ProjectilePool {
private pool: Phaser.GameObjects.Group;
get(): Projectile {
const projectile = this.pool.getFirstDead(true);
if (projectile) {
projectile.setActive(true).setVisible(true);
return projectile;
}
return this.pool.add(new Projectile(this.scene));
}
release(projectile: Projectile) {
projectile.setActive(false).setVisible(false);
projectile.body.stop();
}
}
```
### Culling
- Entities outside viewport are deactivated
- Physics calculations only for active entities
- Particle systems use burst mode, not continuous
### Asset Loading
- Sprites loaded as atlases
- Audio preloaded in PreloadScene
- Level templates loaded on demand
---
## Testing Strategy
### Unit Tests
- State management logic
- Level generation algorithms
- Upgrade calculations
- Collision detection helpers
### Integration Tests
- Scene transitions
- Save/load functionality
- Input handling
### Manual Testing
- Game feel tuning
- Balance verification
- Performance profiling