---
id: wiki-2026-0508-stage-director-and-world-tension
title: Stage Director and World Tension Scaling
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [AI Director, Dynamic Difficulty, Tension Curve]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [game-design, ai-director, pacing, dda]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: typescript
  framework: nodejs
---

# Stage Director and World Tension Scaling

## 매 한 줄
> **"매 AI Director 의 player tension 의 dynamic shape"**. 매 Left 4 Dead 의 emergent pacing 의 tradition — 매 enemy spawn, intensity, encounter density 의 player state 의 따라 modulate. 매 2026 의 trend: ML-driven anomaly detection + scripted fallback.

## 매 핵심

### 매 정의
- **Stage Director**: 매 game state 의 monitor + encounter authoring 의 high-level controller.
- **Tension**: numerical proxy of player stress (HP, ammo, time-since-fight, group cohesion).
- **Scaling**: spawn rate, enemy tier, environmental hazard 의 tension 의 따라 적응.

### 매 input signals
- HP delta over last 30s.
- Ammo deficit ratio.
- Time-since-last-encounter.
- Squad cohesion (player distance).
- Audio volume / heart rate (advanced biometric).

### 매 응용
1. Co-op shooter 의 spawn director (L4D, Vermintide).
2. Survival horror 의 jump scare 의 spacing.
3. Roguelike 의 boss room 의 difficulty 의 player skill 의 따라 swap.

## 💻 패턴

### Tension model
```typescript
interface PlayerState {
  hp: number; maxHp: number;
  ammoRatio: number;
  secondsSinceFight: number;
  squadSpread: number; // meters
}

export function tensionScore(p: PlayerState): number {
  const hpDeficit = 1 - p.hp / p.maxHp;
  const ammoDeficit = 1 - p.ammoRatio;
  const restBonus = Math.min(1, p.secondsSinceFight / 60);
  const spreadStress = Math.min(1, p.squadSpread / 30);
  // 매 tension 의 0..1 의 normalize
  return clamp01(0.4 * hpDeficit + 0.3 * ammoDeficit + 0.3 * spreadStress - 0.5 * restBonus);
}

const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
```

### Director state machine
```typescript
type DirectorPhase = 'build_up' | 'peak' | 'relax' | 'pause';

export class StageDirector {
  phase: DirectorPhase = 'build_up';
  phaseStart = Date.now();

  step(tension: number, now = Date.now()) {
    const elapsed = (now - this.phaseStart) / 1000;
    switch (this.phase) {
      case 'build_up': if (tension > 0.7) this.go('peak', now); break;
      case 'peak':     if (elapsed > 45 || tension > 0.95) this.go('relax', now); break;
      case 'relax':    if (elapsed > 30) this.go('pause', now); break;
      case 'pause':    if (elapsed > 60 || tension < 0.2) this.go('build_up', now); break;
    }
  }

  private go(p: DirectorPhase, now: number) {
    this.phase = p;
    this.phaseStart = now;
  }
}
```

### Spawn budget
```typescript
const SPAWN_BUDGET: Record<DirectorPhase, { rate: number; maxAlive: number }> = {
  build_up: { rate: 0.5, maxAlive: 6 },
  peak:     { rate: 2.0, maxAlive: 20 },
  relax:    { rate: 0.2, maxAlive: 3 },
  pause:    { rate: 0.0, maxAlive: 0 },
};

export function shouldSpawn(phase: DirectorPhase, alive: number, dt: number): boolean {
  const cfg = SPAWN_BUDGET[phase];
  if (alive >= cfg.maxAlive) return false;
  return Math.random() < cfg.rate * dt;
}
```

### Encounter selector (weighted)
```typescript
interface Encounter { id: string; tensionMin: number; tensionMax: number; weight: number; }

export function pickEncounter(pool: Encounter[], tension: number): Encounter | null {
  const eligible = pool.filter(e => tension >= e.tensionMin && tension <= e.tensionMax);
  const total = eligible.reduce((s, e) => s + e.weight, 0);
  if (total === 0) return null;
  let r = Math.random() * total;
  for (const e of eligible) { r -= e.weight; if (r <= 0) return e; }
  return eligible.at(-1) ?? null;
}
```

### ML-assisted anomaly (LightGBM stub)
```typescript
// 매 player flow 의 deviate detection
import { predict } from './lgbm-runtime';

export async function flowAnomaly(features: number[]): Promise<number> {
  // returns 0 (smooth) .. 1 (anomaly)
  return predict('director_anomaly_v3', features);
}
```

### Audio cue layer
```typescript
export function selectMusicLayer(phase: DirectorPhase): string {
  return {
    build_up: 'ambient_low',
    peak:     'combat_full',
    relax:    'aftermath_strings',
    pause:    'silence',
  }[phase];
}
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| 매 horror game | Long pause + sudden peak — 매 jump scare 의 efficacy. |
| 매 co-op shooter | Build-up → peak → relax 의 4-min cycle. |
| 매 boss arena | Disable director — scripted authoring. |
| 매 narrative beat | Force pause — cutscene 의 pacing 의 protect. |

**기본값**: 4-phase state machine + tension proxy + spawn budget.

## 🔗 Graph
- 부모: [[Player-Experience-Modeling]] · [[Gamification-Theory]]
- 변형: [[State-Machine-and-Phase-Transition-Events]] · [[Live Operations (LiveOps)]]
- 응용: [[War-Commander-Combat-Ecosystem]] · [[Roguelike Procedural Generation]]
- Adjacent: [[Procedural-Level-Geometry]] · [[Procedural-Rhetoric|Procedural Rhetoric (In Gaming)]]

## 🤖 LLM 활용
**언제**: encounter copy generation, off-line analysis 의 director log 의 narrative summary.
**언제 X**: 매 tick-rate director decision (latency-sensitive — local FSM).

## ❌ 안티패턴
- **Peak fatigue**: 매 sustained peak 의 burnout — 매 mandatory relax window 누락.
- **Pure RNG**: 매 director 없음 — 매 player flow 의 ignore.
- **Visible threshold**: 매 player 의 director gaming — feel of agency 의 erode.

## 🧪 검증 / 중복
- Verified: Left 4 Dead 2 director GDC talks (Booth), Alien: Isolation AI dual-system, Vermintide 2 patches.
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — director FSM + tension proxy 추가 |
