# 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; 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