Files
chicken-game/docs/ARCHITECTURE.md
2026-01-17 11:49:36 +03:00

25 KiB

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
// 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:

// 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

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

interface SaveData {
  version: string;           // For migration
  metaState: MetaState;
  lastPlayed: number;        // Timestamp
}

TypeScript Interfaces

Core Types

// 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

// 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

// 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

// 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

// 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

// 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

// GameScene.ts
hitStop(duration: number = 50) {
  this.physics.pause();
  this.time.delayedCall(duration, () => {
    this.physics.resume();
  });
}

Performance Considerations

Object Pooling

// 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