945 lines
25 KiB
Markdown
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
|