Skip to main content

proof_engine/game/
music.rs

1//! Chaos RPG procedural music engine integration.
2//!
3//! Wires the proof-engine's procedural music system into the game, providing
4//! vibe-based dynamic music, corruption-driven audio degradation, floor-depth
5//! progression, boss-specific music controllers, and audio-reactive visual
6//! bindings.  The `MusicDirector` is the top-level orchestrator that owns every
7//! subsystem and is ticked each frame by the game loop.
8
9use std::f32::consts::{PI, TAU};
10
11use crate::audio::music_engine::{
12    Chord, MelodyGenerator, MusicEngine, NoteEvent, NoteVoice, Progression,
13    RhythmPattern, Scale, ScaleType, VibeConfig,
14};
15
16// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17// Constants
18// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19
20/// Internal sample rate used for per-sample corruption DSP (matches synth.rs).
21const SAMPLE_RATE: f32 = 48_000.0;
22
23/// Default crossfade time in seconds for vibe transitions.
24const DEFAULT_CROSSFADE_SECS: f32 = 0.75;
25
26/// Maximum number of music layers in the stack.
27const MAX_LAYERS: usize = 4;
28
29/// FFT size used for audio analysis (must be power of two).
30const FFT_SIZE: usize = 1024;
31
32// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33// GameVibe — high-level music state enum
34// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35
36/// Every distinct musical mood the game can be in.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum GameVibe {
39    TitleScreen,
40    Exploration,
41    Combat,
42    Boss,
43    Shop,
44    Shrine,
45    ChaosRift,
46    LowHP,
47    Death,
48    Victory,
49}
50
51impl GameVibe {
52    /// Return the static configuration for this vibe.
53    pub fn config(self) -> GameVibeConfig {
54        match self {
55            GameVibe::TitleScreen => VIBE_CONFIGS[0].clone(),
56            GameVibe::Exploration => VIBE_CONFIGS[1].clone(),
57            GameVibe::Combat      => VIBE_CONFIGS[2].clone(),
58            GameVibe::Boss        => VIBE_CONFIGS[3].clone(),
59            GameVibe::Shop        => VIBE_CONFIGS[4].clone(),
60            GameVibe::Shrine      => VIBE_CONFIGS[5].clone(),
61            GameVibe::ChaosRift   => VIBE_CONFIGS[6].clone(),
62            GameVibe::LowHP       => VIBE_CONFIGS[7].clone(),
63            GameVibe::Death       => VIBE_CONFIGS[8].clone(),
64            GameVibe::Victory     => VIBE_CONFIGS[9].clone(),
65        }
66    }
67
68    /// Convert to a `VibeConfig` compatible with the core `MusicEngine`.
69    pub fn to_engine_vibe(self) -> VibeConfig {
70        let gc = self.config();
71        let root_midi = note_name_to_midi(gc.key_root);
72        let scale = Scale::new(root_midi, gc.scale_type);
73
74        let progression = match self {
75            GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
76                Progression::new(vec![
77                    (Chord::triad_major(3), 8.0),
78                    (Chord::sus2(3), 8.0),
79                ])
80            }
81            GameVibe::Exploration | GameVibe::Shop | GameVibe::Victory => {
82                Progression::one_five_six_four(3)
83            }
84            GameVibe::Combat | GameVibe::LowHP => Progression::minor_pop(3),
85            GameVibe::Boss => Progression::two_five_one(2),
86            GameVibe::ChaosRift => Progression::new(vec![
87                (Chord::diminished(3), 4.0),
88                (Chord::augmented(3), 4.0),
89                (Chord::seventh(3), 4.0),
90                (Chord::sus4(3), 4.0),
91            ]),
92        };
93
94        let rhythm = match self {
95            GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
96                RhythmPattern::new(vec![0.0, 2.0], 4.0)
97            }
98            GameVibe::Exploration | GameVibe::Shop => RhythmPattern::waltz(),
99            GameVibe::Combat | GameVibe::LowHP => RhythmPattern::four_on_floor(),
100            GameVibe::Boss => RhythmPattern::syncopated(),
101            GameVibe::ChaosRift => RhythmPattern::clave_son(),
102            GameVibe::Victory => RhythmPattern::eighth_notes(),
103        };
104
105        let (bass, melody, pad, arp) = match self {
106            GameVibe::TitleScreen => (false, false, true, false),
107            GameVibe::Exploration => (true, true, true, false),
108            GameVibe::Combat      => (true, true, false, true),
109            GameVibe::Boss        => (true, true, true, true),
110            GameVibe::Shop        => (true, true, true, false),
111            GameVibe::Shrine      => (false, false, true, false),
112            GameVibe::ChaosRift   => (true, true, false, true),
113            GameVibe::LowHP       => (false, true, false, false),
114            GameVibe::Death       => (false, false, true, false),
115            GameVibe::Victory     => (true, true, false, false),
116        };
117
118        VibeConfig {
119            scale,
120            bpm: gc.tempo_bpm,
121            progression,
122            rhythm,
123            bass_enabled: bass,
124            melody_enabled: melody,
125            pad_enabled: pad,
126            arp_enabled: arp,
127            volume: gc.volume,
128            spaciousness: gc.reverb_amount,
129        }
130    }
131}
132
133// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
134// GameVibeConfig
135// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
136
137/// Static parameters that define a musical mood.
138#[derive(Clone, Debug)]
139pub struct GameVibeConfig {
140    pub scale_type: ScaleType,
141    pub key_root: &'static str,
142    pub tempo_bpm: f32,
143    pub time_signature: (u8, u8),
144    pub instrument_set: InstrumentSet,
145    pub reverb_amount: f32,
146    pub filter_cutoff: f32,
147    pub volume: f32,
148}
149
150/// Broad instrument palette for a vibe.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub enum InstrumentSet {
153    EtherealPads,
154    Melodic,
155    PercussionHeavy,
156    HeavyBass,
157    WarmGentle,
158    Ethereal,
159    RandomChaos,
160    ThinArrangement,
161    Minimal,
162    Triumphant,
163}
164
165// ── Static vibe table ────────────────────────────────────────────────────────
166
167/// One `GameVibeConfig` per `GameVibe` variant, in enum order.
168static VIBE_CONFIGS: &[GameVibeConfig] = &[
169    // TitleScreen
170    GameVibeConfig {
171        scale_type: ScaleType::Pentatonic,
172        key_root: "C",
173        tempo_bpm: 72.0,
174        time_signature: (4, 4),
175        instrument_set: InstrumentSet::EtherealPads,
176        reverb_amount: 0.85,
177        filter_cutoff: 2000.0,
178        volume: 0.55,
179    },
180    // Exploration
181    GameVibeConfig {
182        scale_type: ScaleType::Major,
183        key_root: "G",
184        tempo_bpm: 110.0,
185        time_signature: (4, 4),
186        instrument_set: InstrumentSet::Melodic,
187        reverb_amount: 0.6,
188        filter_cutoff: 8000.0,
189        volume: 0.65,
190    },
191    // Combat
192    GameVibeConfig {
193        scale_type: ScaleType::NaturalMinor,
194        key_root: "D",
195        tempo_bpm: 140.0,
196        time_signature: (4, 4),
197        instrument_set: InstrumentSet::PercussionHeavy,
198        reverb_amount: 0.25,
199        filter_cutoff: 12000.0,
200        volume: 0.80,
201    },
202    // Boss
203    GameVibeConfig {
204        scale_type: ScaleType::Diminished,
205        key_root: "Bb",
206        tempo_bpm: 160.0,
207        time_signature: (4, 4),
208        instrument_set: InstrumentSet::HeavyBass,
209        reverb_amount: 0.20,
210        filter_cutoff: 14000.0,
211        volume: 1.0,
212    },
213    // Shop
214    GameVibeConfig {
215        scale_type: ScaleType::Major,
216        key_root: "F",
217        tempo_bpm: 90.0,
218        time_signature: (4, 4),
219        instrument_set: InstrumentSet::WarmGentle,
220        reverb_amount: 0.5,
221        filter_cutoff: 5000.0,
222        volume: 0.5,
223    },
224    // Shrine
225    GameVibeConfig {
226        scale_type: ScaleType::WholeTone,
227        key_root: "E",
228        tempo_bpm: 60.0,
229        time_signature: (4, 4),
230        instrument_set: InstrumentSet::Ethereal,
231        reverb_amount: 0.95,
232        filter_cutoff: 1500.0,
233        volume: 0.45,
234    },
235    // ChaosRift
236    GameVibeConfig {
237        scale_type: ScaleType::Chromatic,
238        key_root: "C",          // overridden at runtime with random root
239        tempo_bpm: 120.0,
240        time_signature: (4, 4),
241        instrument_set: InstrumentSet::RandomChaos,
242        reverb_amount: 0.4,
243        filter_cutoff: 10000.0,
244        volume: 0.7,
245    },
246    // LowHP
247    GameVibeConfig {
248        scale_type: ScaleType::NaturalMinor,
249        key_root: "D",          // shifts from current
250        tempo_bpm: 119.0,       // -15% applied dynamically
251        time_signature: (4, 4),
252        instrument_set: InstrumentSet::ThinArrangement,
253        reverb_amount: 0.3,
254        filter_cutoff: 3000.0,
255        volume: 0.5,
256    },
257    // Death
258    GameVibeConfig {
259        scale_type: ScaleType::Phrygian,
260        key_root: "A",
261        tempo_bpm: 50.0,
262        time_signature: (4, 4),
263        instrument_set: InstrumentSet::Minimal,
264        reverb_amount: 0.9,
265        filter_cutoff: 1000.0,
266        volume: 0.35,
267    },
268    // Victory
269    GameVibeConfig {
270        scale_type: ScaleType::Major,
271        key_root: "C",
272        tempo_bpm: 130.0,
273        time_signature: (4, 4),
274        instrument_set: InstrumentSet::Triumphant,
275        reverb_amount: 0.45,
276        filter_cutoff: 10000.0,
277        volume: 0.85,
278    },
279];
280
281// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
282// Note-name helper
283// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
284
285/// Convert a note name such as `"C"`, `"Bb"`, `"F#"` to a MIDI number in
286/// octave 4 (middle octave).
287fn note_name_to_midi(name: &str) -> u8 {
288    let base = match name.chars().next().unwrap_or('C') {
289        'C' => 0,
290        'D' => 2,
291        'E' => 4,
292        'F' => 5,
293        'G' => 7,
294        'A' => 9,
295        'B' => 11,
296        _   => 0,
297    };
298    let modifier: i8 = if name.contains('#') {
299        1
300    } else if name.contains('b') {
301        -1
302    } else {
303        0
304    };
305    ((60 + base) as i8 + modifier).clamp(0, 127) as u8
306}
307
308// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
309// LayerType + MusicLayer
310// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311
312/// Which role a layer fulfils in the mix.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum LayerType {
315    BassDrone,
316    Melody,
317    Percussion,
318    FullArrangement,
319    Ambient,
320    Tension,
321}
322
323/// One layer in the dynamic music stack.
324#[derive(Clone, Debug)]
325pub struct MusicLayer {
326    pub layer_type: LayerType,
327    pub volume: f32,
328    pub target_volume: f32,
329    pub crossfade_rate: f32,
330    pub active: bool,
331    /// Base frequency for drone layers.
332    pub base_freq: f32,
333    /// Pattern data — indices into a scale for melody/percussion.
334    pub pattern: Vec<i32>,
335    /// Current step in the pattern.
336    pub pattern_cursor: usize,
337    /// Beats elapsed since last pattern step.
338    pub beat_accumulator: f32,
339    /// Beats per pattern step (reciprocal of note density).
340    pub step_beats: f32,
341}
342
343impl MusicLayer {
344    pub fn new(layer_type: LayerType) -> Self {
345        Self {
346            layer_type,
347            volume: 0.0,
348            target_volume: 0.0,
349            crossfade_rate: 2.0, // full fade in 0.5 s at 60 fps
350            active: false,
351            base_freq: 65.41, // C2
352            pattern: Vec::new(),
353            pattern_cursor: 0,
354            beat_accumulator: 0.0,
355            step_beats: 1.0,
356        }
357    }
358
359    /// Drive the volume toward `target_volume` at `crossfade_rate` per second.
360    pub fn update(&mut self, dt: f32) {
361        if (self.volume - self.target_volume).abs() < 0.001 {
362            self.volume = self.target_volume;
363        } else if self.volume < self.target_volume {
364            self.volume = (self.volume + self.crossfade_rate * dt).min(self.target_volume);
365        } else {
366            self.volume = (self.volume - self.crossfade_rate * dt).max(self.target_volume);
367        }
368        if self.volume < 0.001 && self.target_volume < 0.001 {
369            self.active = false;
370        }
371    }
372
373    /// Fade this layer in over `secs` seconds.
374    pub fn fade_in(&mut self, secs: f32) {
375        self.active = true;
376        self.target_volume = 1.0;
377        self.crossfade_rate = 1.0 / secs.max(0.01);
378    }
379
380    /// Fade this layer out over `secs` seconds.
381    pub fn fade_out(&mut self, secs: f32) {
382        self.target_volume = 0.0;
383        self.crossfade_rate = 1.0 / secs.max(0.01);
384    }
385
386    /// Advance pattern playback by `beat_delta` beats. Returns note events.
387    pub fn tick_pattern(&mut self, beat_delta: f32, scale: &Scale) -> Vec<NoteEvent> {
388        let mut events = Vec::new();
389        if !self.active || self.pattern.is_empty() {
390            return events;
391        }
392        self.beat_accumulator += beat_delta;
393        while self.beat_accumulator >= self.step_beats {
394            self.beat_accumulator -= self.step_beats;
395            let degree = self.pattern[self.pattern_cursor % self.pattern.len()];
396            self.pattern_cursor = (self.pattern_cursor + 1) % self.pattern.len();
397
398            let octave = match self.layer_type {
399                LayerType::BassDrone => 2,
400                LayerType::Melody => 5,
401                LayerType::Percussion => 3,
402                LayerType::FullArrangement => 4,
403                LayerType::Ambient => 4,
404                LayerType::Tension => 3,
405            };
406
407            let voice = match self.layer_type {
408                LayerType::BassDrone => NoteVoice::Bass,
409                LayerType::Melody => NoteVoice::Melody,
410                LayerType::Percussion => NoteVoice::Chord,
411                LayerType::FullArrangement => NoteVoice::Pad,
412                LayerType::Ambient => NoteVoice::Pad,
413                LayerType::Tension => NoteVoice::Arp,
414            };
415
416            events.push(NoteEvent {
417                frequency: scale.freq(degree, octave),
418                amplitude: self.volume * 0.6,
419                duration: self.step_beats * 0.8,
420                pan: match self.layer_type {
421                    LayerType::BassDrone => 0.0,
422                    LayerType::Melody => 0.2,
423                    LayerType::Percussion => -0.1,
424                    LayerType::FullArrangement => 0.0,
425                    LayerType::Ambient => -0.3,
426                    LayerType::Tension => 0.4,
427                },
428                voice,
429            });
430        }
431        events
432    }
433}
434
435// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
436// MusicLayerStack
437// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
438
439/// Four cross-fadable music layers driven by the current `GameVibe`.
440///
441/// - Layer 0 (always): bass drone tuned to floor depth
442/// - Layer 1 (exploration): procedural melody pattern
443/// - Layer 2 (combat): percussion + rhythm pattern
444/// - Layer 3 (boss): full arrangement
445#[derive(Clone, Debug)]
446pub struct MusicLayerStack {
447    pub layers: [MusicLayer; MAX_LAYERS],
448    pub current_vibe: GameVibe,
449    pub current_scale: Scale,
450    pub beats_per_second: f32,
451}
452
453impl MusicLayerStack {
454    pub fn new() -> Self {
455        let mut layers = [
456            MusicLayer::new(LayerType::BassDrone),
457            MusicLayer::new(LayerType::Melody),
458            MusicLayer::new(LayerType::Percussion),
459            MusicLayer::new(LayerType::FullArrangement),
460        ];
461
462        // Bass drone default pattern: root and fifth
463        layers[0].pattern = vec![0, 0, 4, 0];
464        layers[0].step_beats = 2.0;
465
466        // Melody default: pentatonic run
467        layers[1].pattern = vec![0, 2, 4, 5, 7, 5, 4, 2];
468        layers[1].step_beats = 0.5;
469
470        // Percussion: alternating root/fifth for rhythmic hits
471        layers[2].pattern = vec![0, 0, 4, 0, 0, 4, 0, 4];
472        layers[2].step_beats = 0.25;
473
474        // Full arrangement: chord tones
475        layers[3].pattern = vec![0, 2, 4, 7, 4, 2, 0, -1];
476        layers[3].step_beats = 0.5;
477
478        Self {
479            layers,
480            current_vibe: GameVibe::TitleScreen,
481            current_scale: Scale::new(60, ScaleType::Pentatonic),
482            beats_per_second: 72.0 / 60.0,
483        }
484    }
485
486    /// Transition to a new vibe, cross-fading layers over `crossfade_secs`.
487    pub fn transition_to(&mut self, vibe: GameVibe, crossfade_secs: f32) {
488        let cfg = vibe.config();
489        self.current_vibe = vibe;
490        self.current_scale = Scale::new(note_name_to_midi(cfg.key_root), cfg.scale_type);
491        self.beats_per_second = cfg.tempo_bpm / 60.0;
492
493        let secs = crossfade_secs.max(0.05);
494
495        match vibe {
496            GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
497                self.layers[0].fade_in(secs);
498                self.layers[1].fade_out(secs);
499                self.layers[2].fade_out(secs);
500                self.layers[3].fade_out(secs);
501            }
502            GameVibe::Exploration | GameVibe::Shop | GameVibe::Victory => {
503                self.layers[0].fade_in(secs);
504                self.layers[1].fade_in(secs);
505                self.layers[2].fade_out(secs);
506                self.layers[3].fade_out(secs);
507            }
508            GameVibe::Combat | GameVibe::LowHP | GameVibe::ChaosRift => {
509                self.layers[0].fade_in(secs);
510                self.layers[1].fade_in(secs);
511                self.layers[2].fade_in(secs);
512                self.layers[3].fade_out(secs);
513            }
514            GameVibe::Boss => {
515                self.layers[0].fade_in(secs);
516                self.layers[1].fade_in(secs);
517                self.layers[2].fade_in(secs);
518                self.layers[3].fade_in(secs);
519            }
520        }
521    }
522
523    /// Adjust bass drone frequency based on floor depth.
524    /// Floor 1 => C2 (65.41 Hz), floor 100 => C0 (16.35 Hz).
525    pub fn set_floor_depth(&mut self, floor: u32) {
526        let floor_clamped = (floor as f32).clamp(1.0, 100.0);
527        // Linear interpolation in MIDI space: C2 (36) down to C0 (12).
528        let midi = 36.0 - (floor_clamped - 1.0) / 99.0 * 24.0;
529        self.layers[0].base_freq = Scale::midi_to_hz(midi.clamp(12.0, 36.0) as u8);
530    }
531
532    /// Tick all layers. Returns accumulated note events.
533    pub fn update(&mut self, dt: f32) -> Vec<NoteEvent> {
534        let beat_delta = dt * self.beats_per_second;
535        let mut events = Vec::new();
536        for layer in &mut self.layers {
537            layer.update(dt);
538            events.extend(layer.tick_pattern(beat_delta, &self.current_scale));
539        }
540        events
541    }
542}
543
544impl Default for MusicLayerStack {
545    fn default() -> Self {
546        Self::new()
547    }
548}
549
550// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
551// Corruption Audio Degradation
552// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
553
554/// Which corruption effects are active at a given corruption level.
555#[derive(Clone, Copy, Debug, PartialEq, Eq)]
556pub enum CorruptionTier {
557    Clean,
558    PitchWobble,
559    RhythmDrift,
560    FilterModulation,
561    GranularArtifacts,
562}
563
564impl CorruptionTier {
565    pub fn from_level(level: u32) -> Self {
566        match level {
567            0..=100   => CorruptionTier::Clean,
568            101..=200 => CorruptionTier::PitchWobble,
569            201..=300 => CorruptionTier::RhythmDrift,
570            301..=400 => CorruptionTier::FilterModulation,
571            _         => CorruptionTier::GranularArtifacts,
572        }
573    }
574}
575
576/// Per-sample pitch wobble effect.
577#[derive(Clone, Debug)]
578pub struct PitchWobble {
579    pub max_cents: f32,
580    pub probability: f32,
581    phase: f32,
582    rng_state: u64,
583    active_offset: f32,
584}
585
586impl PitchWobble {
587    pub fn new() -> Self {
588        Self {
589            max_cents: 20.0,
590            probability: 0.0,
591            phase: 0.0,
592            rng_state: 0xDEAD_BEEF,
593            active_offset: 0.0,
594        }
595    }
596
597    pub fn set_intensity(&mut self, t: f32) {
598        // t in [0, 1] maps corruption 100-200 range
599        self.probability = t.clamp(0.0, 1.0) * 0.3;
600        self.max_cents = 20.0 * t.clamp(0.0, 1.0);
601    }
602
603    fn xorshift(&mut self) -> f32 {
604        self.rng_state ^= self.rng_state << 13;
605        self.rng_state ^= self.rng_state >> 7;
606        self.rng_state ^= self.rng_state << 17;
607        (self.rng_state & 0xFFFF) as f32 / 65535.0
608    }
609
610    /// Apply pitch wobble to a single audio sample (via allpass-style phase shift).
611    pub fn apply(&mut self, sample: f32) -> f32 {
612        self.phase += 1.0 / SAMPLE_RATE;
613        if self.phase > 1.0 {
614            self.phase -= 1.0;
615            // Decide whether to activate wobble this cycle
616            if self.xorshift() < self.probability {
617                self.active_offset = (self.xorshift() * 2.0 - 1.0) * self.max_cents;
618            } else {
619                self.active_offset *= 0.95; // decay
620            }
621        }
622        // Pitch shift approximation: slight delay modulation
623        let shift_ratio = 2.0f32.powf(self.active_offset / 1200.0);
624        sample * shift_ratio
625    }
626}
627
628impl Default for PitchWobble {
629    fn default() -> Self {
630        Self::new()
631    }
632}
633
634/// Rhythm drift — introduces swing and timing jitter.
635#[derive(Clone, Debug)]
636pub struct RhythmDrift {
637    pub swing_amount: f32,
638    pub jitter_amount: f32,
639    rng_state: u64,
640}
641
642impl RhythmDrift {
643    pub fn new() -> Self {
644        Self {
645            swing_amount: 0.0,
646            jitter_amount: 0.0,
647            rng_state: 0xCAFE_BABE,
648        }
649    }
650
651    pub fn set_intensity(&mut self, t: f32) {
652        self.swing_amount = t.clamp(0.0, 1.0) * 0.3;
653        self.jitter_amount = t.clamp(0.0, 1.0) * 0.05;
654    }
655
656    fn xorshift(&mut self) -> f32 {
657        self.rng_state ^= self.rng_state << 13;
658        self.rng_state ^= self.rng_state >> 7;
659        self.rng_state ^= self.rng_state << 17;
660        (self.rng_state & 0xFFFF) as f32 / 65535.0
661    }
662
663    /// Returns a timing offset in beats to apply to the next note.
664    pub fn beat_offset(&mut self, beat_index: u32) -> f32 {
665        let swing = if beat_index % 2 == 1 {
666            self.swing_amount
667        } else {
668            0.0
669        };
670        let jitter = (self.xorshift() * 2.0 - 1.0) * self.jitter_amount;
671        swing + jitter
672    }
673
674    /// Identity pass-through for per-sample usage (drift is applied at note level).
675    pub fn apply(&self, sample: f32) -> f32 {
676        sample
677    }
678}
679
680impl Default for RhythmDrift {
681    fn default() -> Self {
682        Self::new()
683    }
684}
685
686/// LFO-modulated filter cutoff effect.
687#[derive(Clone, Debug)]
688pub struct FilterModulationEffect {
689    pub lfo_rate: f32,
690    pub lfo_depth: f32,
691    pub base_cutoff: f32,
692    phase: f32,
693    // Simple one-pole LPF state
694    prev_output: f32,
695}
696
697impl FilterModulationEffect {
698    pub fn new() -> Self {
699        Self {
700            lfo_rate: 1.0,
701            lfo_depth: 0.0,
702            base_cutoff: 8000.0,
703            phase: 0.0,
704            prev_output: 0.0,
705        }
706    }
707
708    pub fn set_intensity(&mut self, t: f32, rng_seed: u64) {
709        // Rate between 0.1 and 5.0 Hz based on corruption + seed
710        let pseudo = ((rng_seed & 0xFFFF) as f32) / 65535.0;
711        self.lfo_rate = 0.1 + pseudo * 4.9;
712        self.lfo_depth = t.clamp(0.0, 1.0) * 6000.0;
713    }
714
715    /// Apply the modulated filter to a single sample.
716    pub fn apply(&mut self, sample: f32) -> f32 {
717        self.phase += self.lfo_rate / SAMPLE_RATE;
718        if self.phase >= 1.0 {
719            self.phase -= 1.0;
720        }
721        let lfo_val = (self.phase * TAU).sin();
722        let cutoff = (self.base_cutoff + lfo_val * self.lfo_depth).clamp(200.0, 20000.0);
723
724        // One-pole LPF: y[n] = y[n-1] + alpha * (x[n] - y[n-1])
725        let alpha = (TAU * cutoff / SAMPLE_RATE).min(1.0);
726        self.prev_output += alpha * (sample - self.prev_output);
727        self.prev_output
728    }
729}
730
731impl Default for FilterModulationEffect {
732    fn default() -> Self {
733        Self::new()
734    }
735}
736
737/// Granular artifacts — stutter, bit-crush, time-stretch glitches.
738#[derive(Clone, Debug)]
739pub struct GranularArtifacts {
740    pub stutter_probability: f32,
741    pub bit_depth: f32,
742    pub time_stretch_factor: f32,
743    last_sample: f32,
744    rng_state: u64,
745    stutter_counter: u32,
746    stutter_length: u32,
747}
748
749impl GranularArtifacts {
750    pub fn new() -> Self {
751        Self {
752            stutter_probability: 0.0,
753            bit_depth: 16.0,
754            time_stretch_factor: 1.0,
755            last_sample: 0.0,
756            rng_state: 0xBAAD_F00D,
757            stutter_counter: 0,
758            stutter_length: 0,
759        }
760    }
761
762    pub fn set_intensity(&mut self, t: f32) {
763        // t in [0, 1+] maps corruption 400+ range
764        let clamped = t.clamp(0.0, 2.0);
765        self.stutter_probability = 0.1 + clamped * 0.1; // 10-30%
766        // Bit depth: 16 -> 8 -> 4
767        self.bit_depth = (16.0 - clamped * 6.0).clamp(4.0, 16.0);
768        self.time_stretch_factor = 1.0 + clamped * 0.3;
769    }
770
771    fn xorshift(&mut self) -> f32 {
772        self.rng_state ^= self.rng_state << 13;
773        self.rng_state ^= self.rng_state >> 7;
774        self.rng_state ^= self.rng_state << 17;
775        (self.rng_state & 0xFFFF) as f32 / 65535.0
776    }
777
778    /// Apply granular artifacts to a single audio sample.
779    pub fn apply(&mut self, sample: f32) -> f32 {
780        let mut out = sample;
781
782        // Stutter: repeat last sample
783        if self.stutter_counter > 0 {
784            self.stutter_counter -= 1;
785            out = self.last_sample;
786        } else if self.xorshift() < self.stutter_probability {
787            self.stutter_length = (self.xorshift() * 2000.0) as u32 + 100;
788            self.stutter_counter = self.stutter_length;
789            self.last_sample = sample;
790            out = sample;
791        }
792
793        // Bit crush
794        let levels = 2.0f32.powf(self.bit_depth);
795        out = (out * levels).round() / levels;
796
797        out
798    }
799}
800
801impl Default for GranularArtifacts {
802    fn default() -> Self {
803        Self::new()
804    }
805}
806
807/// Master corruption audio processor — owns all degradation effects.
808#[derive(Clone, Debug)]
809pub struct CorruptionAudioProcessor {
810    pub corruption_level: f32,
811    pub tier: CorruptionTier,
812    pub pitch_wobble: PitchWobble,
813    pub rhythm_drift: RhythmDrift,
814    pub filter_mod: FilterModulationEffect,
815    pub granular: GranularArtifacts,
816}
817
818impl CorruptionAudioProcessor {
819    pub fn new() -> Self {
820        Self {
821            corruption_level: 0.0,
822            tier: CorruptionTier::Clean,
823            pitch_wobble: PitchWobble::new(),
824            rhythm_drift: RhythmDrift::new(),
825            filter_mod: FilterModulationEffect::new(),
826            granular: GranularArtifacts::new(),
827        }
828    }
829
830    /// Update all degradation parameters from a corruption value (0-500+).
831    pub fn process_corruption(&mut self, corruption: u32) {
832        self.corruption_level = corruption as f32;
833        self.tier = CorruptionTier::from_level(corruption);
834
835        match self.tier {
836            CorruptionTier::Clean => {
837                self.pitch_wobble.set_intensity(0.0);
838                self.rhythm_drift.set_intensity(0.0);
839                self.filter_mod.set_intensity(0.0, 0);
840                self.granular.set_intensity(0.0);
841            }
842            CorruptionTier::PitchWobble => {
843                let t = (corruption as f32 - 100.0) / 100.0;
844                self.pitch_wobble.set_intensity(t);
845                self.rhythm_drift.set_intensity(0.0);
846                self.filter_mod.set_intensity(0.0, 0);
847                self.granular.set_intensity(0.0);
848            }
849            CorruptionTier::RhythmDrift => {
850                let t = (corruption as f32 - 200.0) / 100.0;
851                self.pitch_wobble.set_intensity(1.0);
852                self.rhythm_drift.set_intensity(t);
853                self.filter_mod.set_intensity(0.0, 0);
854                self.granular.set_intensity(0.0);
855            }
856            CorruptionTier::FilterModulation => {
857                let t = (corruption as f32 - 300.0) / 100.0;
858                self.pitch_wobble.set_intensity(1.0);
859                self.rhythm_drift.set_intensity(1.0);
860                self.filter_mod.set_intensity(t, corruption as u64);
861                self.granular.set_intensity(0.0);
862            }
863            CorruptionTier::GranularArtifacts => {
864                let t = (corruption as f32 - 400.0) / 100.0;
865                self.pitch_wobble.set_intensity(1.0);
866                self.rhythm_drift.set_intensity(1.0);
867                self.filter_mod.set_intensity(1.0, corruption as u64);
868                self.granular.set_intensity(t);
869            }
870        }
871    }
872
873    /// Apply all active corruption effects to a single audio sample.
874    pub fn apply(&mut self, sample: f32) -> f32 {
875        let mut s = sample;
876        if self.tier >= CorruptionTier::PitchWobble {
877            s = self.pitch_wobble.apply(s);
878        }
879        if self.tier >= CorruptionTier::RhythmDrift {
880            s = self.rhythm_drift.apply(s);
881        }
882        if self.tier >= CorruptionTier::FilterModulation {
883            s = self.filter_mod.apply(s);
884        }
885        if self.tier >= CorruptionTier::GranularArtifacts {
886            s = self.granular.apply(s);
887        }
888        s
889    }
890
891    /// Return a beat offset to apply to the next note (rhythm drift).
892    pub fn beat_offset(&mut self, beat_index: u32) -> f32 {
893        if self.tier >= CorruptionTier::RhythmDrift {
894            self.rhythm_drift.beat_offset(beat_index)
895        } else {
896            0.0
897        }
898    }
899}
900
901impl Default for CorruptionAudioProcessor {
902    fn default() -> Self {
903        Self::new()
904    }
905}
906
907/// Enable `>=` comparisons on `CorruptionTier` for tier thresholds.
908impl PartialOrd for CorruptionTier {
909    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
910        Some(self.cmp(other))
911    }
912}
913
914impl Ord for CorruptionTier {
915    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
916        let rank = |t: &CorruptionTier| -> u8 {
917            match t {
918                CorruptionTier::Clean              => 0,
919                CorruptionTier::PitchWobble        => 1,
920                CorruptionTier::RhythmDrift        => 2,
921                CorruptionTier::FilterModulation   => 3,
922                CorruptionTier::GranularArtifacts  => 4,
923            }
924        };
925        rank(self).cmp(&rank(other))
926    }
927}
928
929// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
930// Floor Depth Progression
931// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
932
933/// Chord type used in floor profiles.
934#[derive(Clone, Copy, Debug, PartialEq, Eq)]
935pub enum ChordType {
936    Major,
937    Minor,
938    Diminished,
939    Augmented,
940    Suspended,
941    Power,
942    Seventh,
943}
944
945/// Musical personality of a floor range.
946#[derive(Clone, Debug)]
947pub struct FloorMusicProfile {
948    pub scale: ScaleType,
949    pub chord_types: Vec<ChordType>,
950    pub arrangement_density: f32,
951    pub tempo_modifier: f32,
952    pub reverb: f32,
953    pub special_notes: &'static str,
954}
955
956/// Derive the floor music profile from a floor number.
957pub fn floor_music_profile(floor: u32) -> FloorMusicProfile {
958    match floor {
959        1..=10 => FloorMusicProfile {
960            scale: ScaleType::Major,
961            chord_types: vec![ChordType::Major, ChordType::Minor, ChordType::Suspended],
962            arrangement_density: 1.0,
963            tempo_modifier: 1.0,
964            reverb: 0.35,
965            special_notes: "Warm tones, full arrangement",
966        },
967        11..=25 => FloorMusicProfile {
968            scale: ScaleType::Dorian,
969            chord_types: vec![ChordType::Minor, ChordType::Seventh, ChordType::Suspended],
970            arrangement_density: 0.85,
971            tempo_modifier: 1.0,
972            reverb: 0.45,
973            special_notes: "Dorian mode, slightly cooler, steady tempo",
974        },
975        26..=50 => FloorMusicProfile {
976            scale: ScaleType::NaturalMinor,
977            chord_types: vec![ChordType::Minor, ChordType::Power],
978            arrangement_density: 0.6,
979            tempo_modifier: 0.95,
980            reverb: 0.55,
981            special_notes: "Minor, thinner, sparse percussion",
982        },
983        51..=75 => FloorMusicProfile {
984            scale: ScaleType::Diminished,
985            chord_types: vec![ChordType::Diminished, ChordType::Minor, ChordType::Augmented],
986            arrangement_density: 0.4,
987            tempo_modifier: 0.8,
988            reverb: 0.8,
989            special_notes: "Diminished chords appear, tempo drops 0.8x, long reverb",
990        },
991        76..=99 => FloorMusicProfile {
992            scale: ScaleType::Chromatic,
993            chord_types: vec![ChordType::Power],
994            arrangement_density: 0.15,
995            tempo_modifier: 0.7,
996            reverb: 0.9,
997            special_notes: "Atonal, percussion = heartbeat only (sine 60 BPM), minimal melody",
998        },
999        _ => FloorMusicProfile {
1000            // 100+
1001            scale: ScaleType::WholeTone,
1002            chord_types: vec![],
1003            arrangement_density: 0.02,
1004            tempo_modifier: 0.5,
1005            reverb: 0.99,
1006            special_notes: "Near silence, single breathing sine drone, calm before The Algorithm",
1007        },
1008    }
1009}
1010
1011/// Apply a `FloorMusicProfile` to a `MusicLayerStack` and engine.
1012pub fn apply_floor_profile(
1013    stack: &mut MusicLayerStack,
1014    engine: &mut MusicEngine,
1015    floor: u32,
1016) {
1017    let profile = floor_music_profile(floor);
1018    stack.set_floor_depth(floor);
1019
1020    // Adjust engine tempo
1021    let base_bpm = engine.current_bpm();
1022    let adjusted_bpm = base_bpm * profile.tempo_modifier;
1023    stack.beats_per_second = adjusted_bpm / 60.0;
1024
1025    // For deep floors (76-99), strip layers to heartbeat only
1026    if floor >= 76 && floor <= 99 {
1027        stack.layers[1].fade_out(1.0); // no melody
1028        stack.layers[2].fade_out(1.0); // no percussion layers
1029        stack.layers[3].fade_out(1.0); // no arrangement
1030
1031        // Heartbeat pattern: single note at 60 BPM = 1 beat/s
1032        stack.layers[0].pattern = vec![0];
1033        stack.layers[0].step_beats = 1.0;
1034    } else if floor >= 100 {
1035        // Near silence — single breathing drone
1036        stack.layers[0].pattern = vec![0];
1037        stack.layers[0].step_beats = 4.0;
1038        stack.layers[1].fade_out(2.0);
1039        stack.layers[2].fade_out(2.0);
1040        stack.layers[3].fade_out(2.0);
1041    }
1042}
1043
1044// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1045// Boss-Specific Music
1046// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1047
1048/// The four named bosses in Chaos RPG.
1049#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1050pub enum BossMusic {
1051    Mirror,
1052    Null,
1053    Committee,
1054    AlgorithmReborn,
1055}
1056
1057/// Player combat action categories (for AlgorithmReborn Phase 2 adaptation).
1058#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1059pub enum PlayerActionType {
1060    Melee,
1061    Magic,
1062    Defense,
1063}
1064
1065/// Per-boss music controller.
1066#[derive(Clone, Debug)]
1067pub struct BossMusicController {
1068    pub boss: Option<BossMusic>,
1069    /// Note sequence buffer (for Mirror's reverse melody).
1070    pub note_buffer: Vec<f32>,
1071    /// Max notes to buffer for Mirror boss reverse playback.
1072    pub buffer_capacity: usize,
1073    /// Reverse playback cursor.
1074    pub reverse_cursor: usize,
1075    /// How many layers are currently active (for Null boss stripping).
1076    pub active_layer_count: u32,
1077    /// Null boss HP fraction at which the last layer was stripped.
1078    pub last_strip_hp: f32,
1079    /// Time signature numerator (for Committee's 5/4).
1080    pub time_sig_numerator: u8,
1081    /// AlgorithmReborn phase (1, 2, or 3).
1082    pub algorithm_phase: u8,
1083    /// Player's most-used action (for AlgorithmReborn Phase 2).
1084    pub dominant_action: PlayerActionType,
1085    /// Action counts for tracking.
1086    pub action_counts: [u32; 3],
1087}
1088
1089impl BossMusicController {
1090    pub fn new() -> Self {
1091        Self {
1092            boss: None,
1093            note_buffer: Vec::with_capacity(256),
1094            buffer_capacity: 256,
1095            reverse_cursor: 0,
1096            active_layer_count: 4,
1097            last_strip_hp: 1.0,
1098            time_sig_numerator: 4,
1099            algorithm_phase: 1,
1100            dominant_action: PlayerActionType::Melee,
1101            action_counts: [0; 3],
1102        }
1103    }
1104
1105    /// Activate boss-specific music behaviour.
1106    pub fn activate(&mut self, boss: BossMusic, stack: &mut MusicLayerStack) {
1107        self.boss = Some(boss);
1108        self.note_buffer.clear();
1109        self.reverse_cursor = 0;
1110        self.active_layer_count = 4;
1111        self.last_strip_hp = 1.0;
1112        self.algorithm_phase = 1;
1113        self.action_counts = [0; 3];
1114
1115        match boss {
1116            BossMusic::Mirror => {
1117                // Melody plays backward — we buffer notes and read in reverse
1118            }
1119            BossMusic::Null => {
1120                // All 4 layers start active; stripped as HP drops
1121                for layer in &mut stack.layers {
1122                    layer.fade_in(0.5);
1123                }
1124                self.active_layer_count = 4;
1125            }
1126            BossMusic::Committee => {
1127                // 5/4 time signature — adjust pattern lengths
1128                self.time_sig_numerator = 5;
1129                for layer in &mut stack.layers {
1130                    // Extend patterns to 5 beats per measure
1131                    layer.step_beats = layer.step_beats * 5.0 / 4.0;
1132                }
1133            }
1134            BossMusic::AlgorithmReborn => {
1135                // Phase 1: normal boss music
1136                self.algorithm_phase = 1;
1137            }
1138        }
1139    }
1140
1141    /// Deactivate boss music (combat over).
1142    pub fn deactivate(&mut self) {
1143        self.boss = None;
1144        self.time_sig_numerator = 4;
1145    }
1146
1147    /// Feed a generated note into the Mirror boss's reverse buffer.
1148    pub fn mirror_buffer_note(&mut self, freq: f32) {
1149        if self.boss != Some(BossMusic::Mirror) {
1150            return;
1151        }
1152        if self.note_buffer.len() >= self.buffer_capacity {
1153            self.note_buffer.remove(0);
1154        }
1155        self.note_buffer.push(freq);
1156    }
1157
1158    /// Get the next note from the Mirror boss's reversed buffer.
1159    pub fn mirror_next_reversed(&mut self) -> Option<f32> {
1160        if self.boss != Some(BossMusic::Mirror) || self.note_buffer.is_empty() {
1161            return None;
1162        }
1163        let idx = self.note_buffer.len() - 1 - (self.reverse_cursor % self.note_buffer.len());
1164        self.reverse_cursor += 1;
1165        Some(self.note_buffer[idx])
1166    }
1167
1168    /// Null boss: strip a layer when HP crosses a 10% threshold.
1169    pub fn null_update_hp(&mut self, hp_fraction: f32, stack: &mut MusicLayerStack) {
1170        if self.boss != Some(BossMusic::Null) {
1171            return;
1172        }
1173        // Strip a layer every 10% HP lost
1174        let threshold = self.last_strip_hp - 0.1;
1175        if hp_fraction < threshold && self.active_layer_count > 0 {
1176            self.last_strip_hp = hp_fraction;
1177            // Fade out the highest active layer
1178            let layer_idx = (self.active_layer_count as usize).min(MAX_LAYERS) - 1;
1179            stack.layers[layer_idx].fade_out(0.8);
1180            self.active_layer_count = self.active_layer_count.saturating_sub(1);
1181        }
1182    }
1183
1184    /// Record a player action for AlgorithmReborn adaptation.
1185    pub fn record_action(&mut self, action: PlayerActionType) {
1186        let idx = match action {
1187            PlayerActionType::Melee   => 0,
1188            PlayerActionType::Magic   => 1,
1189            PlayerActionType::Defense => 2,
1190        };
1191        self.action_counts[idx] += 1;
1192
1193        // Determine dominant action
1194        let max_idx = self
1195            .action_counts
1196            .iter()
1197            .enumerate()
1198            .max_by_key(|(_, &c)| c)
1199            .map(|(i, _)| i)
1200            .unwrap_or(0);
1201        self.dominant_action = match max_idx {
1202            0 => PlayerActionType::Melee,
1203            1 => PlayerActionType::Magic,
1204            _ => PlayerActionType::Defense,
1205        };
1206    }
1207
1208    /// AlgorithmReborn: advance to the next phase.
1209    pub fn algorithm_advance_phase(&mut self, stack: &mut MusicLayerStack) {
1210        if self.boss != Some(BossMusic::AlgorithmReborn) {
1211            return;
1212        }
1213        self.algorithm_phase = (self.algorithm_phase + 1).min(3);
1214        match self.algorithm_phase {
1215            2 => {
1216                // Phase 2 — adapt to player's most-used action
1217                match self.dominant_action {
1218                    PlayerActionType::Melee => {
1219                        // Heavy percussion
1220                        stack.layers[2].fade_in(0.3);
1221                        stack.layers[2].step_beats = 0.125; // 32nd notes
1222                    }
1223                    PlayerActionType::Magic => {
1224                        // Arpeggios
1225                        stack.layers[1].pattern =
1226                            vec![0, 2, 4, 7, 9, 11, 9, 7, 4, 2];
1227                        stack.layers[1].step_beats = 0.125;
1228                        stack.layers[1].fade_in(0.3);
1229                    }
1230                    PlayerActionType::Defense => {
1231                        // Minimal — strip melody and arp
1232                        stack.layers[1].fade_out(0.5);
1233                        stack.layers[3].fade_out(0.5);
1234                    }
1235                }
1236            }
1237            3 => {
1238                // Phase 3 — all dissonant + granular
1239                stack.current_scale = Scale::new(
1240                    stack.current_scale.root,
1241                    ScaleType::Chromatic,
1242                );
1243                // All layers active but dissonant patterns
1244                for layer in &mut stack.layers {
1245                    layer.pattern = vec![0, 1, 6, 7, 1, 11, 5, 6];
1246                    layer.fade_in(0.2);
1247                }
1248            }
1249            _ => {}
1250        }
1251    }
1252
1253    /// Process notes through boss-specific transformations.
1254    pub fn process_notes(
1255        &mut self,
1256        notes: &mut Vec<NoteEvent>,
1257        stack: &mut MusicLayerStack,
1258    ) {
1259        let boss = match self.boss {
1260            Some(b) => b,
1261            None => return,
1262        };
1263
1264        match boss {
1265            BossMusic::Mirror => {
1266                // Buffer all melody notes, then replace with reversed
1267                let melody_notes: Vec<f32> = notes
1268                    .iter()
1269                    .filter(|n| n.voice == NoteVoice::Melody)
1270                    .map(|n| n.frequency)
1271                    .collect();
1272                for freq in &melody_notes {
1273                    self.mirror_buffer_note(*freq);
1274                }
1275                // Replace melody frequencies with reversed buffer
1276                for note in notes.iter_mut() {
1277                    if note.voice == NoteVoice::Melody {
1278                        if let Some(rev_freq) = self.mirror_next_reversed() {
1279                            note.frequency = rev_freq;
1280                        }
1281                    }
1282                }
1283            }
1284            BossMusic::Committee => {
1285                // Time signature already applied in activate()
1286                // No per-note transformation needed
1287            }
1288            BossMusic::AlgorithmReborn if self.algorithm_phase == 3 => {
1289                // Add extra dissonance: shift every other note by a tritone
1290                let mut toggle = false;
1291                for note in notes.iter_mut() {
1292                    if toggle {
1293                        note.frequency *= 2.0f32.powf(6.0 / 12.0); // tritone
1294                    }
1295                    toggle = !toggle;
1296                }
1297            }
1298            _ => {}
1299        }
1300    }
1301}
1302
1303impl Default for BossMusicController {
1304    fn default() -> Self {
1305        Self::new()
1306    }
1307}
1308
1309// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1310// Audio-Reactive Visual Binding
1311// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1312
1313/// Frequency-band energy and transient analysis for visual binding.
1314#[derive(Clone, Debug, Default)]
1315pub struct AudioAnalysis {
1316    pub bass_energy: f32,
1317    pub mid_energy: f32,
1318    pub high_energy: f32,
1319    pub beat_detected: bool,
1320    pub envelope: f32,
1321    pub spectral_centroid: f32,
1322}
1323
1324/// Visual state that the audio-visual bridge writes into.
1325#[derive(Clone, Debug)]
1326pub struct GameVisuals {
1327    pub chaos_particle_speed_mult: f32,
1328    pub camera_fov_offset: f32,
1329    pub force_field_strength: f32,
1330    pub entity_emission_pulse: f32,
1331    pub vignette_intensity: f32,
1332}
1333
1334impl Default for GameVisuals {
1335    fn default() -> Self {
1336        Self {
1337            chaos_particle_speed_mult: 1.0,
1338            camera_fov_offset: 0.0,
1339            force_field_strength: 0.0,
1340            entity_emission_pulse: 0.0,
1341            vignette_intensity: 0.5,
1342        }
1343    }
1344}
1345
1346/// Bridges audio analysis to game visuals.
1347#[derive(Clone, Debug)]
1348pub struct AudioVisualBridge {
1349    /// Smoothed bass energy for particles.
1350    smoothed_bass: f32,
1351    /// Beat pulse timer (decays after beat detection).
1352    beat_pulse_timer: f32,
1353    /// Previous envelope for beat detection (onset).
1354    prev_envelope: f32,
1355    /// Beat detection threshold.
1356    beat_threshold: f32,
1357    /// Running history for spectral flux onset detection.
1358    prev_band_energies: [f32; 3],
1359}
1360
1361impl AudioVisualBridge {
1362    pub fn new() -> Self {
1363        Self {
1364            smoothed_bass: 0.0,
1365            beat_pulse_timer: 0.0,
1366            prev_envelope: 0.0,
1367            beat_threshold: 0.15,
1368            prev_band_energies: [0.0; 3],
1369        }
1370    }
1371
1372    /// Compute an `AudioAnalysis` from a raw audio buffer using FFT-like band
1373    /// energy estimation.
1374    ///
1375    /// For efficiency we use a simplified DFT across three bands rather than a
1376    /// full FFT (the game runs at 60 fps and needs this every frame).
1377    pub fn compute_analysis(&mut self, audio_buffer: &[f32], sample_rate: u32) -> AudioAnalysis {
1378        if audio_buffer.is_empty() {
1379            return AudioAnalysis::default();
1380        }
1381
1382        let sr = sample_rate as f32;
1383        let n = audio_buffer.len();
1384
1385        // ── Band energies via Goertzel-style targeted DFT ────────────────
1386        //
1387        // Bass: 20-250 Hz
1388        // Mid:  250-4000 Hz
1389        // High: 4000-16000 Hz
1390        let bass = band_energy(audio_buffer, sr, 20.0, 250.0);
1391        let mid = band_energy(audio_buffer, sr, 250.0, 4000.0);
1392        let high = band_energy(audio_buffer, sr, 4000.0, 16000.0);
1393
1394        // ── Envelope (RMS) ───────────────────────────────────────────────
1395        let rms = (audio_buffer.iter().map(|s| s * s).sum::<f32>() / n as f32).sqrt();
1396
1397        // ── Beat detection (spectral flux) ───────────────────────────────
1398        let flux = (bass - self.prev_band_energies[0]).max(0.0)
1399            + (mid - self.prev_band_energies[1]).max(0.0);
1400        let beat_detected = flux > self.beat_threshold;
1401        self.prev_band_energies = [bass, mid, high];
1402
1403        // ── Spectral centroid ────────────────────────────────────────────
1404        let total_e = bass + mid + high + 1e-10;
1405        let centroid = (bass * 135.0 + mid * 2125.0 + high * 10000.0) / total_e;
1406
1407        self.prev_envelope = rms;
1408
1409        AudioAnalysis {
1410            bass_energy: bass,
1411            mid_energy: mid,
1412            high_energy: high,
1413            beat_detected,
1414            envelope: rms,
1415            spectral_centroid: centroid,
1416        }
1417    }
1418
1419    /// Write the audio analysis results into the game's visual state.
1420    pub fn apply_to_visuals(
1421        &mut self,
1422        analysis: &AudioAnalysis,
1423        visuals: &mut GameVisuals,
1424        dt: f32,
1425    ) {
1426        // Smooth bass for particle speed
1427        self.smoothed_bass += (analysis.bass_energy - self.smoothed_bass) * (dt * 8.0).min(1.0);
1428        visuals.chaos_particle_speed_mult = 1.0 + self.smoothed_bass * 2.0;
1429
1430        // Beat-detected FOV micro-pulse
1431        if analysis.beat_detected {
1432            self.beat_pulse_timer = 0.1;
1433        }
1434        if self.beat_pulse_timer > 0.0 {
1435            visuals.camera_fov_offset = -0.005; // -0.5%
1436            self.beat_pulse_timer -= dt;
1437        } else {
1438            visuals.camera_fov_offset = 0.0;
1439        }
1440
1441        // Mid energy -> force field oscillation
1442        visuals.force_field_strength = analysis.mid_energy * 1.5;
1443
1444        // High energy -> entity glyph emission
1445        visuals.entity_emission_pulse = analysis.high_energy * 2.0;
1446
1447        // Envelope -> vignette (louder = less vignette)
1448        visuals.vignette_intensity = (0.6 - analysis.envelope).clamp(0.1, 0.8);
1449    }
1450}
1451
1452impl Default for AudioVisualBridge {
1453    fn default() -> Self {
1454        Self::new()
1455    }
1456}
1457
1458/// Estimate band energy using a lightweight Goertzel-style approach.
1459///
1460/// Sums the energy of a few representative frequencies within the band.
1461fn band_energy(buf: &[f32], sample_rate: f32, lo_hz: f32, hi_hz: f32) -> f32 {
1462    let n = buf.len() as f32;
1463    let num_probes = 4u32;
1464    let mut total = 0.0f32;
1465    for i in 0..num_probes {
1466        let freq = lo_hz + (hi_hz - lo_hz) * (i as f32 + 0.5) / num_probes as f32;
1467        let k = (freq * n / sample_rate).round();
1468        let w = TAU * k / n;
1469        // Goertzel
1470        let mut s0 = 0.0f32;
1471        let mut s1 = 0.0f32;
1472        let mut s2: f32;
1473        let coeff = 2.0 * w.cos();
1474        for &x in buf {
1475            s2 = s1;
1476            s1 = s0;
1477            s0 = x + coeff * s1 - s2;
1478        }
1479        let power = s0 * s0 + s1 * s1 - coeff * s0 * s1;
1480        total += power.abs();
1481    }
1482    (total / (num_probes as f32 * n)).sqrt()
1483}
1484
1485// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1486// ChaosRift random key change tracker
1487// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1488
1489/// Tracks bar count for ChaosRift random key changes every 4 bars.
1490#[derive(Clone, Debug)]
1491pub struct ChaosRiftTracker {
1492    pub bar_count: u32,
1493    pub last_change_bar: u32,
1494    rng_state: u64,
1495}
1496
1497impl ChaosRiftTracker {
1498    pub fn new() -> Self {
1499        Self {
1500            bar_count: 0,
1501            last_change_bar: 0,
1502            rng_state: 0xC0FFEE,
1503        }
1504    }
1505
1506    fn xorshift(&mut self) -> u64 {
1507        self.rng_state ^= self.rng_state << 13;
1508        self.rng_state ^= self.rng_state >> 7;
1509        self.rng_state ^= self.rng_state << 17;
1510        self.rng_state
1511    }
1512
1513    /// Call once per bar. Returns a new random MIDI root if a key change is due.
1514    pub fn tick_bar(&mut self) -> Option<u8> {
1515        self.bar_count += 1;
1516        if self.bar_count - self.last_change_bar >= 4 {
1517            self.last_change_bar = self.bar_count;
1518            let root = (self.xorshift() % 12) as u8 + 48; // C3..B3
1519            Some(root)
1520        } else {
1521            None
1522        }
1523    }
1524}
1525
1526impl Default for ChaosRiftTracker {
1527    fn default() -> Self {
1528        Self::new()
1529    }
1530}
1531
1532// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1533// Room type helper
1534// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1535
1536/// Room types that map to vibes.
1537#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1538pub enum RoomType {
1539    Normal,
1540    Shop,
1541    Shrine,
1542    ChaosRift,
1543    BossArena,
1544}
1545
1546/// Enemy difficulty tier for combat music intensity.
1547#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1548pub enum EnemyTier {
1549    Fodder,
1550    Standard,
1551    Elite,
1552    MiniBoss,
1553}
1554
1555// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1556// MusicDirector — top-level orchestrator
1557// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1558
1559/// The MusicDirector is the single entry point for the game loop.
1560///
1561/// It owns the layer stack, corruption processor, floor profile, boss
1562/// controller, chaos-rift tracker, and the audio-visual bridge. Every
1563/// frame the game calls `update(dt, ...)` which ticks all subsystems.
1564pub struct MusicDirector {
1565    pub engine: MusicEngine,
1566    pub layer_stack: MusicLayerStack,
1567    pub corruption: CorruptionAudioProcessor,
1568    pub boss_controller: BossMusicController,
1569    pub audio_visual_bridge: AudioVisualBridge,
1570    pub chaos_tracker: ChaosRiftTracker,
1571    pub visuals: GameVisuals,
1572    pub current_vibe: GameVibe,
1573    pub current_floor: u32,
1574    /// Accumulated beat counter for the corruption rhythm-drift.
1575    beat_counter: u32,
1576    /// Previous bar number for chaos-rift key changes.
1577    prev_bar: u32,
1578}
1579
1580impl std::fmt::Debug for MusicDirector {
1581    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1582        f.debug_struct("MusicDirector")
1583            .field("current_vibe", &self.current_vibe)
1584            .field("current_floor", &self.current_floor)
1585            .field("layer_stack", &self.layer_stack)
1586            .field("corruption", &self.corruption)
1587            .field("boss_controller", &self.boss_controller)
1588            .finish()
1589    }
1590}
1591
1592impl MusicDirector {
1593    pub fn new() -> Self {
1594        let mut engine = MusicEngine::new();
1595        engine.set_vibe(GameVibe::TitleScreen.to_engine_vibe());
1596
1597        Self {
1598            engine,
1599            layer_stack: MusicLayerStack::new(),
1600            corruption: CorruptionAudioProcessor::new(),
1601            boss_controller: BossMusicController::new(),
1602            audio_visual_bridge: AudioVisualBridge::new(),
1603            chaos_tracker: ChaosRiftTracker::new(),
1604            visuals: GameVisuals::default(),
1605            current_vibe: GameVibe::TitleScreen,
1606            current_floor: 1,
1607            beat_counter: 0,
1608            prev_bar: 0,
1609        }
1610    }
1611
1612    // ── Game event handlers ──────────────────────────────────────────────────
1613
1614    /// Called when the player enters a new room.
1615    pub fn on_enter_room(&mut self, room_type: RoomType, floor: u32) {
1616        self.current_floor = floor;
1617        let vibe = match room_type {
1618            RoomType::Normal    => GameVibe::Exploration,
1619            RoomType::Shop      => GameVibe::Shop,
1620            RoomType::Shrine    => GameVibe::Shrine,
1621            RoomType::ChaosRift => GameVibe::ChaosRift,
1622            RoomType::BossArena => GameVibe::Boss,
1623        };
1624        self.transition_vibe(vibe);
1625        apply_floor_profile(&mut self.layer_stack, &mut self.engine, floor);
1626    }
1627
1628    /// Called when combat begins.
1629    pub fn on_combat_start(&mut self, enemy_tier: EnemyTier) {
1630        let vibe = match enemy_tier {
1631            EnemyTier::Fodder | EnemyTier::Standard => GameVibe::Combat,
1632            EnemyTier::Elite | EnemyTier::MiniBoss => GameVibe::Combat,
1633        };
1634        self.transition_vibe(vibe);
1635
1636        // Increase intensity for elites
1637        if enemy_tier == EnemyTier::Elite || enemy_tier == EnemyTier::MiniBoss {
1638            self.engine.master_volume = 0.9;
1639        }
1640    }
1641
1642    /// Called when a boss encounter starts.
1643    pub fn on_boss_encounter(&mut self, boss_type: BossMusic) {
1644        self.transition_vibe(GameVibe::Boss);
1645        self.boss_controller.activate(boss_type, &mut self.layer_stack);
1646    }
1647
1648    /// Called when combat ends.
1649    pub fn on_combat_end(&mut self) {
1650        self.boss_controller.deactivate();
1651        self.engine.master_volume = 1.0;
1652        self.transition_vibe(GameVibe::Exploration);
1653    }
1654
1655    /// Called when the player drops to low HP.
1656    pub fn on_player_low_hp(&mut self) {
1657        self.transition_vibe(GameVibe::LowHP);
1658        // Reduce tempo by 15%
1659        let current = self.engine.current_bpm();
1660        let reduced = current * 0.85;
1661        self.layer_stack.beats_per_second = reduced / 60.0;
1662    }
1663
1664    /// Called when the player dies.
1665    pub fn on_player_death(&mut self) {
1666        self.boss_controller.deactivate();
1667        self.transition_vibe(GameVibe::Death);
1668    }
1669
1670    /// Called when corruption level changes.
1671    pub fn on_corruption_change(&mut self, level: u32) {
1672        self.corruption.process_corruption(level);
1673    }
1674
1675    /// Called when the player changes floors.
1676    pub fn on_floor_change(&mut self, floor: u32) {
1677        self.current_floor = floor;
1678        self.layer_stack.set_floor_depth(floor);
1679        apply_floor_profile(&mut self.layer_stack, &mut self.engine, floor);
1680    }
1681
1682    /// Called on victory.
1683    pub fn on_victory(&mut self) {
1684        self.boss_controller.deactivate();
1685        self.transition_vibe(GameVibe::Victory);
1686    }
1687
1688    // ── Internal ─────────────────────────────────────────────────────────────
1689
1690    fn transition_vibe(&mut self, vibe: GameVibe) {
1691        if self.current_vibe == vibe {
1692            return;
1693        }
1694        self.current_vibe = vibe;
1695        self.engine.set_vibe(vibe.to_engine_vibe());
1696        self.layer_stack.transition_to(vibe, DEFAULT_CROSSFADE_SECS);
1697
1698        if vibe == GameVibe::ChaosRift {
1699            self.chaos_tracker = ChaosRiftTracker::new();
1700        }
1701    }
1702
1703    // ── Per-frame update ─────────────────────────────────────────────────────
1704
1705    /// Main per-frame tick. Drives every subsystem.
1706    pub fn update(&mut self, dt: f32, audio_buffer: &[f32], sample_rate: u32) {
1707        // 1) Core music engine tick
1708        let mut notes = self.engine.tick(dt);
1709
1710        // 2) Layer stack tick (generates additional layer-based notes)
1711        let layer_notes = self.layer_stack.update(dt);
1712        notes.extend(layer_notes);
1713
1714        // 3) Boss-specific processing
1715        self.boss_controller.process_notes(&mut notes, &mut self.layer_stack);
1716
1717        // 4) ChaosRift random key changes every 4 bars
1718        if self.current_vibe == GameVibe::ChaosRift {
1719            let bar = self.engine.current_bar();
1720            if bar != self.prev_bar {
1721                self.prev_bar = bar;
1722                if let Some(new_root) = self.chaos_tracker.tick_bar() {
1723                    self.layer_stack.current_scale = Scale::new(
1724                        new_root,
1725                        ScaleType::Chromatic,
1726                    );
1727                }
1728            }
1729        }
1730
1731        // 5) Corruption beat offset
1732        self.beat_counter = self.beat_counter.wrapping_add(1);
1733
1734        // 6) Audio analysis for visual binding
1735        let analysis =
1736            self.audio_visual_bridge.compute_analysis(audio_buffer, sample_rate);
1737        self.audio_visual_bridge
1738            .apply_to_visuals(&analysis, &mut self.visuals, dt);
1739    }
1740
1741    /// Access the current visual state (read by the renderer).
1742    pub fn visuals(&self) -> &GameVisuals {
1743        &self.visuals
1744    }
1745}
1746
1747impl Default for MusicDirector {
1748    fn default() -> Self {
1749        Self::new()
1750    }
1751}
1752
1753// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1754// Tests
1755// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1756
1757#[cfg(test)]
1758mod tests {
1759    use super::*;
1760
1761    // ── Vibe transitions ─────────────────────────────────────────────────────
1762
1763    #[test]
1764    fn vibe_configs_have_correct_count() {
1765        assert_eq!(VIBE_CONFIGS.len(), 10);
1766    }
1767
1768    #[test]
1769    fn title_screen_config_values() {
1770        let cfg = GameVibe::TitleScreen.config();
1771        assert_eq!(cfg.scale_type, ScaleType::Pentatonic);
1772        assert!((cfg.tempo_bpm - 72.0).abs() < 0.01);
1773        assert_eq!(cfg.instrument_set, InstrumentSet::EtherealPads);
1774    }
1775
1776    #[test]
1777    fn exploration_config_values() {
1778        let cfg = GameVibe::Exploration.config();
1779        assert_eq!(cfg.scale_type, ScaleType::Major);
1780        assert_eq!(cfg.key_root, "G");
1781        assert!((cfg.tempo_bpm - 110.0).abs() < 0.01);
1782    }
1783
1784    #[test]
1785    fn combat_config_values() {
1786        let cfg = GameVibe::Combat.config();
1787        assert_eq!(cfg.scale_type, ScaleType::NaturalMinor);
1788        assert!((cfg.tempo_bpm - 140.0).abs() < 0.01);
1789    }
1790
1791    #[test]
1792    fn boss_config_values() {
1793        let cfg = GameVibe::Boss.config();
1794        assert_eq!(cfg.scale_type, ScaleType::Diminished);
1795        assert_eq!(cfg.key_root, "Bb");
1796        assert!((cfg.tempo_bpm - 160.0).abs() < 0.01);
1797    }
1798
1799    #[test]
1800    fn vibe_to_engine_vibe_produces_valid_config() {
1801        let vc = GameVibe::Combat.to_engine_vibe();
1802        assert!(vc.bpm > 100.0);
1803        assert!(vc.bass_enabled);
1804        assert!(vc.melody_enabled);
1805    }
1806
1807    #[test]
1808    fn vibe_transition_changes_layers() {
1809        let mut stack = MusicLayerStack::new();
1810        stack.transition_to(GameVibe::Boss, 0.5);
1811        // Boss activates all 4 layers
1812        assert!(stack.layers[0].target_volume > 0.0);
1813        assert!(stack.layers[1].target_volume > 0.0);
1814        assert!(stack.layers[2].target_volume > 0.0);
1815        assert!(stack.layers[3].target_volume > 0.0);
1816    }
1817
1818    #[test]
1819    fn vibe_transition_exploration_disables_percussion_and_arrangement() {
1820        let mut stack = MusicLayerStack::new();
1821        stack.transition_to(GameVibe::Exploration, 0.5);
1822        assert!(stack.layers[0].target_volume > 0.0); // bass
1823        assert!(stack.layers[1].target_volume > 0.0); // melody
1824        assert!(stack.layers[2].target_volume < 0.01); // percussion off
1825        assert!(stack.layers[3].target_volume < 0.01); // arrangement off
1826    }
1827
1828    // ── Corruption levels ────────────────────────────────────────────────────
1829
1830    #[test]
1831    fn corruption_tier_from_level() {
1832        assert_eq!(CorruptionTier::from_level(0), CorruptionTier::Clean);
1833        assert_eq!(CorruptionTier::from_level(50), CorruptionTier::Clean);
1834        assert_eq!(CorruptionTier::from_level(150), CorruptionTier::PitchWobble);
1835        assert_eq!(CorruptionTier::from_level(250), CorruptionTier::RhythmDrift);
1836        assert_eq!(CorruptionTier::from_level(350), CorruptionTier::FilterModulation);
1837        assert_eq!(CorruptionTier::from_level(500), CorruptionTier::GranularArtifacts);
1838    }
1839
1840    #[test]
1841    fn corruption_processor_clean_passthrough() {
1842        let mut proc = CorruptionAudioProcessor::new();
1843        proc.process_corruption(0);
1844        let out = proc.apply(0.5);
1845        assert!((out - 0.5).abs() < 0.01);
1846    }
1847
1848    #[test]
1849    fn corruption_processor_high_level_modifies_signal() {
1850        let mut proc = CorruptionAudioProcessor::new();
1851        proc.process_corruption(450);
1852        // Run many samples — at high corruption the signal is definitely modified
1853        let mut changed = false;
1854        for i in 0..1000 {
1855            let input = (i as f32 * 0.1).sin() * 0.5;
1856            let out = proc.apply(input);
1857            if (out - input).abs() > 0.01 {
1858                changed = true;
1859                break;
1860            }
1861        }
1862        assert!(changed, "Expected corruption to modify the signal");
1863    }
1864
1865    #[test]
1866    fn pitch_wobble_default_is_clean() {
1867        let mut pw = PitchWobble::new();
1868        pw.set_intensity(0.0);
1869        let out = pw.apply(1.0);
1870        assert!((out - 1.0).abs() < 0.01);
1871    }
1872
1873    #[test]
1874    fn granular_bit_crush_reduces_precision() {
1875        let mut ga = GranularArtifacts::new();
1876        ga.bit_depth = 4.0;
1877        ga.stutter_probability = 0.0; // disable stutter for this test
1878        let out = ga.apply(0.123456);
1879        // With 4-bit depth (16 levels), the output should be quantized
1880        let levels = 2.0f32.powf(4.0);
1881        let expected = (0.123456 * levels).round() / levels;
1882        assert!((out - expected).abs() < 0.001);
1883    }
1884
1885    // ── Floor profiles ───────────────────────────────────────────────────────
1886
1887    #[test]
1888    fn floor_profile_early_floors_are_major() {
1889        let profile = floor_music_profile(1);
1890        assert_eq!(profile.scale, ScaleType::Major);
1891        assert!((profile.tempo_modifier - 1.0).abs() < 0.01);
1892    }
1893
1894    #[test]
1895    fn floor_profile_deep_floors_are_sparse() {
1896        let profile = floor_music_profile(80);
1897        assert_eq!(profile.scale, ScaleType::Chromatic);
1898        assert!(profile.arrangement_density < 0.2);
1899    }
1900
1901    #[test]
1902    fn floor_profile_100_plus_near_silence() {
1903        let profile = floor_music_profile(100);
1904        assert!(profile.arrangement_density < 0.05);
1905        assert!(profile.tempo_modifier < 0.6);
1906    }
1907
1908    #[test]
1909    fn floor_depth_adjusts_bass_drone() {
1910        let mut stack = MusicLayerStack::new();
1911        stack.set_floor_depth(1);
1912        let freq_1 = stack.layers[0].base_freq;
1913        stack.set_floor_depth(100);
1914        let freq_100 = stack.layers[0].base_freq;
1915        // Deeper floors should have lower bass
1916        assert!(freq_1 > freq_100, "Floor 1 freq {freq_1} should be > floor 100 freq {freq_100}");
1917    }
1918
1919    // ── Boss music ───────────────────────────────────────────────────────────
1920
1921    #[test]
1922    fn boss_mirror_reverses_melody() {
1923        let mut ctrl = BossMusicController::new();
1924        let mut stack = MusicLayerStack::new();
1925        ctrl.activate(BossMusic::Mirror, &mut stack);
1926
1927        // Buffer some notes
1928        ctrl.mirror_buffer_note(440.0);
1929        ctrl.mirror_buffer_note(550.0);
1930        ctrl.mirror_buffer_note(660.0);
1931
1932        // Reversed should give 660, 550, 440
1933        let n1 = ctrl.mirror_next_reversed().unwrap();
1934        let n2 = ctrl.mirror_next_reversed().unwrap();
1935        let n3 = ctrl.mirror_next_reversed().unwrap();
1936        assert!((n1 - 660.0).abs() < 0.01);
1937        assert!((n2 - 550.0).abs() < 0.01);
1938        assert!((n3 - 440.0).abs() < 0.01);
1939    }
1940
1941    #[test]
1942    fn boss_null_strips_layers_on_hp_loss() {
1943        let mut ctrl = BossMusicController::new();
1944        let mut stack = MusicLayerStack::new();
1945        ctrl.activate(BossMusic::Null, &mut stack);
1946        assert_eq!(ctrl.active_layer_count, 4);
1947
1948        // Lose 15% HP — should strip one layer
1949        ctrl.null_update_hp(0.85, &mut stack);
1950        assert_eq!(ctrl.active_layer_count, 3);
1951
1952        // Lose another 15%
1953        ctrl.null_update_hp(0.70, &mut stack);
1954        assert_eq!(ctrl.active_layer_count, 2);
1955    }
1956
1957    #[test]
1958    fn boss_committee_sets_5_4_time() {
1959        let mut ctrl = BossMusicController::new();
1960        let mut stack = MusicLayerStack::new();
1961        ctrl.activate(BossMusic::Committee, &mut stack);
1962        assert_eq!(ctrl.time_sig_numerator, 5);
1963    }
1964
1965    #[test]
1966    fn boss_algorithm_records_actions() {
1967        let mut ctrl = BossMusicController::new();
1968        let mut stack = MusicLayerStack::new();
1969        ctrl.activate(BossMusic::AlgorithmReborn, &mut stack);
1970
1971        ctrl.record_action(PlayerActionType::Magic);
1972        ctrl.record_action(PlayerActionType::Magic);
1973        ctrl.record_action(PlayerActionType::Melee);
1974
1975        assert_eq!(ctrl.dominant_action, PlayerActionType::Magic);
1976    }
1977
1978    #[test]
1979    fn boss_algorithm_phase_advance() {
1980        let mut ctrl = BossMusicController::new();
1981        let mut stack = MusicLayerStack::new();
1982        ctrl.activate(BossMusic::AlgorithmReborn, &mut stack);
1983        assert_eq!(ctrl.algorithm_phase, 1);
1984
1985        ctrl.algorithm_advance_phase(&mut stack);
1986        assert_eq!(ctrl.algorithm_phase, 2);
1987
1988        ctrl.algorithm_advance_phase(&mut stack);
1989        assert_eq!(ctrl.algorithm_phase, 3);
1990
1991        // Should clamp at 3
1992        ctrl.algorithm_advance_phase(&mut stack);
1993        assert_eq!(ctrl.algorithm_phase, 3);
1994    }
1995
1996    // ── Audio analysis ───────────────────────────────────────────────────────
1997
1998    #[test]
1999    fn audio_analysis_empty_buffer() {
2000        let mut bridge = AudioVisualBridge::new();
2001        let analysis = bridge.compute_analysis(&[], 48000);
2002        assert!(!analysis.beat_detected);
2003        assert!(analysis.envelope < 0.001);
2004    }
2005
2006    #[test]
2007    fn audio_analysis_sine_has_energy() {
2008        let mut bridge = AudioVisualBridge::new();
2009        let sr = 48000u32;
2010        // Generate a 200 Hz sine (should be in the bass band)
2011        let buf: Vec<f32> = (0..1024)
2012            .map(|i| (TAU * 200.0 * i as f32 / sr as f32).sin() * 0.8)
2013            .collect();
2014        let analysis = bridge.compute_analysis(&buf, sr);
2015        assert!(analysis.bass_energy > 0.0, "Expected bass energy from 200 Hz sine");
2016        assert!(analysis.envelope > 0.1, "Expected non-trivial envelope");
2017    }
2018
2019    #[test]
2020    fn audio_visual_bridge_beat_pulse() {
2021        let mut bridge = AudioVisualBridge::new();
2022        let mut visuals = GameVisuals::default();
2023        let analysis = AudioAnalysis {
2024            bass_energy: 0.5,
2025            mid_energy: 0.3,
2026            high_energy: 0.1,
2027            beat_detected: true,
2028            envelope: 0.4,
2029            spectral_centroid: 2000.0,
2030        };
2031        bridge.apply_to_visuals(&analysis, &mut visuals, 1.0 / 60.0);
2032        // FOV should pulse negative
2033        assert!(visuals.camera_fov_offset < 0.0);
2034        // Particle speed > 1
2035        assert!(visuals.chaos_particle_speed_mult > 1.0);
2036    }
2037
2038    // ── MusicDirector integration ────────────────────────────────────────────
2039
2040    #[test]
2041    fn director_initializes_to_title_screen() {
2042        let dir = MusicDirector::new();
2043        assert_eq!(dir.current_vibe, GameVibe::TitleScreen);
2044    }
2045
2046    #[test]
2047    fn director_room_transitions() {
2048        let mut dir = MusicDirector::new();
2049        dir.on_enter_room(RoomType::Shop, 5);
2050        assert_eq!(dir.current_vibe, GameVibe::Shop);
2051
2052        dir.on_enter_room(RoomType::ChaosRift, 10);
2053        assert_eq!(dir.current_vibe, GameVibe::ChaosRift);
2054    }
2055
2056    #[test]
2057    fn director_combat_flow() {
2058        let mut dir = MusicDirector::new();
2059        dir.on_combat_start(EnemyTier::Standard);
2060        assert_eq!(dir.current_vibe, GameVibe::Combat);
2061
2062        dir.on_combat_end();
2063        assert_eq!(dir.current_vibe, GameVibe::Exploration);
2064    }
2065
2066    #[test]
2067    fn director_boss_encounter() {
2068        let mut dir = MusicDirector::new();
2069        dir.on_boss_encounter(BossMusic::Mirror);
2070        assert_eq!(dir.current_vibe, GameVibe::Boss);
2071        assert_eq!(dir.boss_controller.boss, Some(BossMusic::Mirror));
2072    }
2073
2074    #[test]
2075    fn director_low_hp_reduces_tempo() {
2076        let mut dir = MusicDirector::new();
2077        dir.on_enter_room(RoomType::Normal, 1);
2078        let bpm_before = dir.engine.current_bpm();
2079        dir.on_player_low_hp();
2080        let bps_after = dir.layer_stack.beats_per_second;
2081        // The layer stack BPS should be 85% of the LowHP config tempo
2082        assert!(bps_after < bpm_before / 60.0);
2083    }
2084
2085    #[test]
2086    fn director_corruption_propagates() {
2087        let mut dir = MusicDirector::new();
2088        dir.on_corruption_change(250);
2089        assert_eq!(dir.corruption.tier, CorruptionTier::RhythmDrift);
2090    }
2091
2092    #[test]
2093    fn director_floor_change() {
2094        let mut dir = MusicDirector::new();
2095        dir.on_floor_change(50);
2096        assert_eq!(dir.current_floor, 50);
2097    }
2098
2099    #[test]
2100    fn director_update_runs_without_panic() {
2101        let mut dir = MusicDirector::new();
2102        dir.on_enter_room(RoomType::Normal, 1);
2103        // Simulate 60 frames
2104        let buf = vec![0.0f32; 1024];
2105        for _ in 0..60 {
2106            dir.update(1.0 / 60.0, &buf, 48000);
2107        }
2108    }
2109
2110    #[test]
2111    fn director_victory_flow() {
2112        let mut dir = MusicDirector::new();
2113        dir.on_boss_encounter(BossMusic::Null);
2114        assert_eq!(dir.current_vibe, GameVibe::Boss);
2115        dir.on_victory();
2116        assert_eq!(dir.current_vibe, GameVibe::Victory);
2117        assert_eq!(dir.boss_controller.boss, None);
2118    }
2119
2120    // ── Chaos rift key changes ───────────────────────────────────────────────
2121
2122    #[test]
2123    fn chaos_rift_tracker_changes_key_every_4_bars() {
2124        let mut tracker = ChaosRiftTracker::new();
2125        // First 3 bars: no change
2126        assert!(tracker.tick_bar().is_none());
2127        assert!(tracker.tick_bar().is_none());
2128        assert!(tracker.tick_bar().is_none());
2129        // Bar 4: key change
2130        assert!(tracker.tick_bar().is_some());
2131        // Bars 5-7: no change
2132        assert!(tracker.tick_bar().is_none());
2133        assert!(tracker.tick_bar().is_none());
2134        assert!(tracker.tick_bar().is_none());
2135        // Bar 8: key change
2136        assert!(tracker.tick_bar().is_some());
2137    }
2138
2139    // ── Layer crossfade ──────────────────────────────────────────────────────
2140
2141    #[test]
2142    fn layer_crossfade_reaches_target() {
2143        let mut layer = MusicLayer::new(LayerType::Melody);
2144        layer.fade_in(0.5);
2145        // Tick for 1 second at 60 fps
2146        for _ in 0..60 {
2147            layer.update(1.0 / 60.0);
2148        }
2149        assert!(
2150            (layer.volume - 1.0).abs() < 0.05,
2151            "Expected volume ~1.0, got {}",
2152            layer.volume,
2153        );
2154    }
2155
2156    #[test]
2157    fn layer_fade_out_deactivates() {
2158        let mut layer = MusicLayer::new(LayerType::Percussion);
2159        layer.active = true;
2160        layer.volume = 1.0;
2161        layer.fade_out(0.5);
2162        for _ in 0..120 {
2163            layer.update(1.0 / 60.0);
2164        }
2165        assert!(!layer.active);
2166        assert!(layer.volume < 0.01);
2167    }
2168
2169    // ── Note-name helper ─────────────────────────────────────────────────────
2170
2171    #[test]
2172    fn note_name_c_is_60() {
2173        assert_eq!(note_name_to_midi("C"), 60);
2174    }
2175
2176    #[test]
2177    fn note_name_bb_is_70() {
2178        assert_eq!(note_name_to_midi("Bb"), 70);
2179    }
2180
2181    #[test]
2182    fn note_name_f_sharp_is_66() {
2183        assert_eq!(note_name_to_midi("F#"), 66);
2184    }
2185}