Skip to main content

proof_engine/game/
mod.rs

1//! Game Systems Module — top-level game state coordinator
2//!
3//! Provides GameManager, state machine, score system, event bus, session stats,
4//! timers, difficulty configuration, and all top-level game coordination.
5
6pub mod menu;
7pub mod localization;
8pub mod achievements;
9
10use std::collections::{HashMap, VecDeque};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13// ─── Load Progress ─────────────────────────────────────────────────────────────
14
15/// Describes progress through a multi-stage loading operation.
16#[derive(Debug, Clone)]
17pub struct LoadProgress {
18    pub stage: String,
19    pub current: u32,
20    pub total: u32,
21    pub sub_progress: f32,
22}
23
24impl LoadProgress {
25    pub fn new(stage: impl Into<String>, total: u32) -> Self {
26        Self {
27            stage: stage.into(),
28            current: 0,
29            total,
30            sub_progress: 0.0,
31        }
32    }
33
34    pub fn fraction(&self) -> f32 {
35        if self.total == 0 {
36            return 1.0;
37        }
38        (self.current as f32 + self.sub_progress) / self.total as f32
39    }
40
41    pub fn advance(&mut self, sub: f32) {
42        self.sub_progress = sub.clamp(0.0, 1.0);
43    }
44
45    pub fn next_stage(&mut self, stage: impl Into<String>) {
46        self.current += 1;
47        self.sub_progress = 0.0;
48        self.stage = stage.into();
49    }
50
51    pub fn is_complete(&self) -> bool {
52        self.current >= self.total
53    }
54}
55
56// ─── Game Over Data ─────────────────────────────────────────────────────────────
57
58#[derive(Debug, Clone)]
59pub struct GameOverData {
60    pub score: u64,
61    pub cause: String,
62    pub survival_time: f64,
63    pub kills: u32,
64    pub level_reached: u32,
65}
66
67impl GameOverData {
68    pub fn new(score: u64, cause: impl Into<String>, survival_time: f64, kills: u32, level_reached: u32) -> Self {
69        Self {
70            score,
71            cause: cause.into(),
72            survival_time,
73            kills,
74            level_reached,
75        }
76    }
77}
78
79// ─── Game State ─────────────────────────────────────────────────────────────────
80
81#[derive(Debug, Clone)]
82pub enum GameState {
83    MainMenu,
84    Loading(LoadProgress),
85    Playing,
86    Paused,
87    GameOver(GameOverData),
88    Credits,
89    Settings,
90}
91
92impl GameState {
93    pub fn name(&self) -> &str {
94        match self {
95            GameState::MainMenu => "MainMenu",
96            GameState::Loading(_) => "Loading",
97            GameState::Playing => "Playing",
98            GameState::Paused => "Paused",
99            GameState::GameOver(_) => "GameOver",
100            GameState::Credits => "Credits",
101            GameState::Settings => "Settings",
102        }
103    }
104
105    pub fn is_playing(&self) -> bool {
106        matches!(self, GameState::Playing | GameState::Paused)
107    }
108
109    pub fn can_pause(&self) -> bool {
110        matches!(self, GameState::Playing)
111    }
112}
113
114// ─── Transition Animation ───────────────────────────────────────────────────────
115
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub enum TransitionKind {
118    Fade,
119    SlideLeft,
120    SlideRight,
121    SlideUp,
122    SlideDown,
123    Dissolve,
124    Wipe,
125    Crossfade,
126    None,
127}
128
129#[derive(Debug, Clone)]
130pub struct GameTransition {
131    pub kind: TransitionKind,
132    pub duration: f32,
133    pub elapsed: f32,
134    pub from: String,
135    pub to: String,
136    pub complete: bool,
137}
138
139impl GameTransition {
140    pub fn new(kind: TransitionKind, duration: f32, from: impl Into<String>, to: impl Into<String>) -> Self {
141        Self {
142            kind,
143            duration,
144            elapsed: 0.0,
145            from: from.into(),
146            to: to.into(),
147            complete: false,
148        }
149    }
150
151    pub fn none() -> Self {
152        Self::new(TransitionKind::None, 0.0, "", "")
153    }
154
155    pub fn progress(&self) -> f32 {
156        if self.duration <= 0.0 {
157            return 1.0;
158        }
159        (self.elapsed / self.duration).clamp(0.0, 1.0)
160    }
161
162    pub fn tick(&mut self, dt: f32) {
163        self.elapsed += dt;
164        if self.elapsed >= self.duration {
165            self.complete = true;
166        }
167    }
168
169    pub fn alpha(&self) -> f32 {
170        match self.kind {
171            TransitionKind::Fade | TransitionKind::Crossfade => {
172                let p = self.progress();
173                if p < 0.5 { p * 2.0 } else { (1.0 - p) * 2.0 }
174            }
175            TransitionKind::Dissolve => self.progress(),
176            _ => 1.0,
177        }
178    }
179
180    pub fn offset_x(&self) -> f32 {
181        let p = self.progress();
182        let ease = 1.0 - (1.0 - p).powi(3); // ease-out cubic
183        match self.kind {
184            TransitionKind::SlideLeft => -ease,
185            TransitionKind::SlideRight => ease,
186            _ => 0.0,
187        }
188    }
189
190    pub fn offset_y(&self) -> f32 {
191        let p = self.progress();
192        let ease = 1.0 - (1.0 - p).powi(3);
193        match self.kind {
194            TransitionKind::SlideUp => -ease,
195            TransitionKind::SlideDown => ease,
196            _ => 0.0,
197        }
198    }
199}
200
201// ─── Difficulty Preset ──────────────────────────────────────────────────────────
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204pub enum DifficultyPreset {
205    Story,
206    Easy,
207    Normal,
208    Hard,
209    Expert,
210    Custom,
211}
212
213impl DifficultyPreset {
214    pub fn name(&self) -> &str {
215        match self {
216            DifficultyPreset::Story => "Story",
217            DifficultyPreset::Easy => "Easy",
218            DifficultyPreset::Normal => "Normal",
219            DifficultyPreset::Hard => "Hard",
220            DifficultyPreset::Expert => "Expert",
221            DifficultyPreset::Custom => "Custom",
222        }
223    }
224
225    pub fn all() -> &'static [DifficultyPreset] {
226        &[
227            DifficultyPreset::Story,
228            DifficultyPreset::Easy,
229            DifficultyPreset::Normal,
230            DifficultyPreset::Hard,
231            DifficultyPreset::Expert,
232        ]
233    }
234
235    pub fn default_params(&self) -> DifficultyParams {
236        match self {
237            DifficultyPreset::Story => DifficultyParams {
238                damage_scale: 0.4,
239                enemy_health_scale: 0.5,
240                enemy_speed_scale: 0.7,
241                resource_scale: 2.0,
242                xp_scale: 1.5,
243            },
244            DifficultyPreset::Easy => DifficultyParams {
245                damage_scale: 0.7,
246                enemy_health_scale: 0.75,
247                enemy_speed_scale: 0.85,
248                resource_scale: 1.5,
249                xp_scale: 1.25,
250            },
251            DifficultyPreset::Normal => DifficultyParams::default(),
252            DifficultyPreset::Hard => DifficultyParams {
253                damage_scale: 1.5,
254                enemy_health_scale: 1.5,
255                enemy_speed_scale: 1.2,
256                resource_scale: 0.8,
257                xp_scale: 1.5,
258            },
259            DifficultyPreset::Expert => DifficultyParams {
260                damage_scale: 2.0,
261                enemy_health_scale: 2.5,
262                enemy_speed_scale: 1.4,
263                resource_scale: 0.6,
264                xp_scale: 2.0,
265            },
266            DifficultyPreset::Custom => DifficultyParams::default(),
267        }
268    }
269}
270
271#[derive(Debug, Clone)]
272pub struct DifficultyParams {
273    pub damage_scale: f32,
274    pub enemy_health_scale: f32,
275    pub enemy_speed_scale: f32,
276    pub resource_scale: f32,
277    pub xp_scale: f32,
278}
279
280impl Default for DifficultyParams {
281    fn default() -> Self {
282        Self {
283            damage_scale: 1.0,
284            enemy_health_scale: 1.0,
285            enemy_speed_scale: 1.0,
286            resource_scale: 1.0,
287            xp_scale: 1.0,
288        }
289    }
290}
291
292impl DifficultyParams {
293    pub fn scale_damage(&self, base: f32) -> f32 {
294        base * self.damage_scale
295    }
296
297    pub fn scale_enemy_health(&self, base: f32) -> f32 {
298        base * self.enemy_health_scale
299    }
300
301    pub fn scale_enemy_speed(&self, base: f32) -> f32 {
302        base * self.enemy_speed_scale
303    }
304
305    pub fn scale_resource(&self, base: f32) -> f32 {
306        base * self.resource_scale
307    }
308
309    pub fn scale_xp(&self, base: f32) -> f32 {
310        base * self.xp_scale
311    }
312}
313
314// ─── Game Config ────────────────────────────────────────────────────────────────
315
316#[derive(Debug, Clone)]
317pub struct GameConfig {
318    pub difficulty_preset: DifficultyPreset,
319    pub difficulty_params: DifficultyParams,
320    pub target_fps: u32,
321    pub fullscreen: bool,
322    pub vsync: bool,
323    pub master_volume: f32,
324    pub music_volume: f32,
325    pub sfx_volume: f32,
326    pub voice_volume: f32,
327    pub show_fps: bool,
328    pub show_damage_numbers: bool,
329    pub screen_shake: bool,
330    pub colorblind_mode: bool,
331    pub high_contrast: bool,
332    pub reduce_motion: bool,
333    pub large_text: bool,
334    pub subtitles: bool,
335}
336
337impl Default for GameConfig {
338    fn default() -> Self {
339        Self {
340            difficulty_preset: DifficultyPreset::Normal,
341            difficulty_params: DifficultyParams::default(),
342            target_fps: 60,
343            fullscreen: false,
344            vsync: true,
345            master_volume: 1.0,
346            music_volume: 0.8,
347            sfx_volume: 1.0,
348            voice_volume: 1.0,
349            show_fps: false,
350            show_damage_numbers: true,
351            screen_shake: true,
352            colorblind_mode: false,
353            high_contrast: false,
354            reduce_motion: false,
355            large_text: false,
356            subtitles: false,
357        }
358    }
359}
360
361impl GameConfig {
362    pub fn set_difficulty(&mut self, preset: DifficultyPreset) {
363        self.difficulty_preset = preset;
364        if preset != DifficultyPreset::Custom {
365            self.difficulty_params = preset.default_params();
366        }
367    }
368
369    pub fn effective_volume(&self, channel: VolumeChannel) -> f32 {
370        let base = match channel {
371            VolumeChannel::Music => self.music_volume,
372            VolumeChannel::Sfx => self.sfx_volume,
373            VolumeChannel::Voice => self.voice_volume,
374        };
375        base * self.master_volume
376    }
377}
378
379#[derive(Debug, Clone, Copy)]
380pub enum VolumeChannel {
381    Music,
382    Sfx,
383    Voice,
384}
385
386// ─── Game Timer ─────────────────────────────────────────────────────────────────
387
388#[derive(Debug, Clone)]
389pub struct GameTimer {
390    pub elapsed: f64,
391    pub paused_elapsed: f64,
392    pub session_count: u32,
393    pub first_played_at: u64,
394    paused: bool,
395}
396
397impl GameTimer {
398    pub fn new() -> Self {
399        let now = SystemTime::now()
400            .duration_since(UNIX_EPOCH)
401            .unwrap_or_default()
402            .as_secs();
403        Self {
404            elapsed: 0.0,
405            paused_elapsed: 0.0,
406            session_count: 0,
407            first_played_at: now,
408            paused: false,
409        }
410    }
411
412    pub fn tick(&mut self, dt: f64) {
413        if self.paused {
414            self.paused_elapsed += dt;
415        } else {
416            self.elapsed += dt;
417        }
418    }
419
420    pub fn pause(&mut self) {
421        self.paused = true;
422    }
423
424    pub fn resume(&mut self) {
425        self.paused = false;
426    }
427
428    pub fn is_paused(&self) -> bool {
429        self.paused
430    }
431
432    pub fn total_elapsed(&self) -> f64 {
433        self.elapsed + self.paused_elapsed
434    }
435
436    pub fn start_session(&mut self) {
437        self.session_count += 1;
438    }
439
440    pub fn format_elapsed(&self) -> String {
441        let secs = self.elapsed as u64;
442        let hours = secs / 3600;
443        let minutes = (secs % 3600) / 60;
444        let seconds = secs % 60;
445        if hours > 0 {
446            format!("{}:{:02}:{:02}", hours, minutes, seconds)
447        } else {
448            format!("{}:{:02}", minutes, seconds)
449        }
450    }
451}
452
453impl Default for GameTimer {
454    fn default() -> Self {
455        Self::new()
456    }
457}
458
459// ─── Session Stats ──────────────────────────────────────────────────────────────
460
461#[derive(Debug, Clone, Default)]
462pub struct SessionStats {
463    pub enemies_killed: u32,
464    pub damage_dealt: f64,
465    pub damage_taken: f64,
466    pub distance_traveled: f64,
467    pub items_collected: u32,
468    pub gold_earned: u64,
469    pub gold_spent: u64,
470    pub quests_completed: u32,
471    pub deaths: u32,
472    pub highest_combo: u32,
473    pub critical_hits: u32,
474    pub skills_used: u32,
475    pub spells_cast: u32,
476    pub chests_opened: u32,
477    pub secrets_found: u32,
478    pub levels_visited: u32,
479    pub items_crafted: u32,
480    pub boss_kills: u32,
481    pub playtime_secs: f64,
482    pub max_level_reached: u32,
483    pub highest_score: u64,
484}
485
486impl SessionStats {
487    pub fn new() -> Self {
488        Self::default()
489    }
490
491    pub fn record_kill(&mut self, is_boss: bool) {
492        self.enemies_killed += 1;
493        if is_boss {
494            self.boss_kills += 1;
495        }
496    }
497
498    pub fn record_damage_dealt(&mut self, amount: f64, is_crit: bool) {
499        self.damage_dealt += amount;
500        if is_crit {
501            self.critical_hits += 1;
502        }
503    }
504
505    pub fn record_damage_taken(&mut self, amount: f64) {
506        self.damage_taken += amount;
507    }
508
509    pub fn record_death(&mut self) {
510        self.deaths += 1;
511    }
512
513    pub fn record_item_collected(&mut self) {
514        self.items_collected += 1;
515    }
516
517    pub fn record_gold(&mut self, earned: u64, spent: u64) {
518        self.gold_earned += earned;
519        self.gold_spent += spent;
520    }
521
522    pub fn update_combo(&mut self, combo: u32) {
523        if combo > self.highest_combo {
524            self.highest_combo = combo;
525        }
526    }
527
528    pub fn k_d_ratio(&self) -> f32 {
529        if self.deaths == 0 {
530            return self.enemies_killed as f32;
531        }
532        self.enemies_killed as f32 / self.deaths as f32
533    }
534
535    pub fn accuracy(&self) -> f32 {
536        if self.skills_used == 0 {
537            return 0.0;
538        }
539        self.critical_hits as f32 / self.skills_used as f32
540    }
541}
542
543// ─── Score System ───────────────────────────────────────────────────────────────
544
545#[derive(Debug, Clone, Default)]
546pub struct Score {
547    pub base: u64,
548    pub combo_bonus: u64,
549    pub time_bonus: u64,
550    pub style_bonus: u64,
551    pub total: u64,
552}
553
554impl Score {
555    pub fn new(base: u64) -> Self {
556        Self { base, total: base, ..Default::default() }
557    }
558
559    pub fn calculate_total(&mut self) {
560        self.total = self.base + self.combo_bonus + self.time_bonus + self.style_bonus;
561    }
562
563    pub fn add_combo_bonus(&mut self, bonus: u64) {
564        self.combo_bonus += bonus;
565        self.calculate_total();
566    }
567
568    pub fn add_time_bonus(&mut self, bonus: u64) {
569        self.time_bonus += bonus;
570        self.calculate_total();
571    }
572
573    pub fn add_style_bonus(&mut self, bonus: u64) {
574        self.style_bonus += bonus;
575        self.calculate_total();
576    }
577
578    pub fn add_base(&mut self, amount: u64) {
579        self.base += amount;
580        self.calculate_total();
581    }
582
583    pub fn grade(&self) -> char {
584        match self.total {
585            0..=999 => 'F',
586            1000..=4999 => 'D',
587            5000..=9999 => 'C',
588            10000..=24999 => 'B',
589            25000..=49999 => 'A',
590            50000..=99999 => 'S',
591            _ => 'X',
592        }
593    }
594}
595
596#[derive(Debug, Clone)]
597pub struct ComboTracker {
598    pub count: u32,
599    pub multiplier: f32,
600    pub decay_timer: f32,
601    decay_rate: f32,
602    max_multiplier: f32,
603    combo_window: f32,
604}
605
606impl ComboTracker {
607    pub fn new() -> Self {
608        Self {
609            count: 0,
610            multiplier: 1.0,
611            decay_timer: 0.0,
612            decay_rate: 0.5,
613            max_multiplier: 8.0,
614            combo_window: 3.0,
615        }
616    }
617
618    pub fn with_decay_rate(mut self, rate: f32) -> Self {
619        self.decay_rate = rate;
620        self
621    }
622
623    pub fn with_max_multiplier(mut self, max: f32) -> Self {
624        self.max_multiplier = max;
625        self
626    }
627
628    pub fn with_combo_window(mut self, window: f32) -> Self {
629        self.combo_window = window;
630        self
631    }
632
633    pub fn hit(&mut self) -> f32 {
634        self.count += 1;
635        self.decay_timer = self.combo_window;
636        self.multiplier = self.calculate_multiplier();
637        self.multiplier
638    }
639
640    fn calculate_multiplier(&self) -> f32 {
641        let mult = 1.0 + (self.count as f32 / 10.0).ln_1p() * 2.0;
642        mult.min(self.max_multiplier)
643    }
644
645    pub fn tick(&mut self, dt: f32) {
646        if self.decay_timer > 0.0 {
647            self.decay_timer -= dt;
648            if self.decay_timer <= 0.0 {
649                self.reset();
650            }
651        }
652    }
653
654    pub fn reset(&mut self) {
655        self.count = 0;
656        self.multiplier = 1.0;
657        self.decay_timer = 0.0;
658    }
659
660    pub fn apply_to_score(&self, base_score: u64) -> u64 {
661        (base_score as f32 * self.multiplier) as u64
662    }
663
664    pub fn is_active(&self) -> bool {
665        self.count > 0 && self.decay_timer > 0.0
666    }
667}
668
669impl Default for ComboTracker {
670    fn default() -> Self {
671        Self::new()
672    }
673}
674
675#[derive(Debug, Clone)]
676pub struct ScoreEntry {
677    pub name: String,
678    pub score: u64,
679    pub date: u64,
680    pub metadata: HashMap<String, String>,
681}
682
683impl ScoreEntry {
684    pub fn new(name: impl Into<String>, score: u64) -> Self {
685        let date = SystemTime::now()
686            .duration_since(UNIX_EPOCH)
687            .unwrap_or_default()
688            .as_secs();
689        Self {
690            name: name.into(),
691            score,
692            date,
693            metadata: HashMap::new(),
694        }
695    }
696
697    pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
698        self.metadata.insert(key.into(), value.into());
699        self
700    }
701}
702
703#[derive(Debug, Clone)]
704pub struct HighScoreTable {
705    pub entries: Vec<ScoreEntry>,
706    pub max_entries: usize,
707}
708
709impl HighScoreTable {
710    pub fn new(max_entries: usize) -> Self {
711        Self {
712            entries: Vec::new(),
713            max_entries,
714        }
715    }
716
717    pub fn add(&mut self, entry: ScoreEntry) -> usize {
718        let rank = self.entries.iter().position(|e| e.score < entry.score)
719            .unwrap_or(self.entries.len());
720        self.entries.insert(rank, entry);
721        if self.entries.len() > self.max_entries {
722            self.entries.truncate(self.max_entries);
723        }
724        rank + 1
725    }
726
727    pub fn rank_of(&self, score: u64) -> Option<usize> {
728        for (i, entry) in self.entries.iter().enumerate() {
729            if score >= entry.score {
730                return Some(i + 1);
731            }
732        }
733        if self.entries.len() < self.max_entries {
734            Some(self.entries.len() + 1)
735        } else {
736            None
737        }
738    }
739
740    pub fn is_high_score(&self, score: u64) -> bool {
741        self.rank_of(score).is_some()
742    }
743
744    pub fn top_score(&self) -> Option<u64> {
745        self.entries.first().map(|e| e.score)
746    }
747
748    pub fn format_leaderboard(&self) -> Vec<String> {
749        self.entries.iter().enumerate().map(|(i, e)| {
750            format!("{:3}. {:20} {:>12}", i + 1, e.name, e.score)
751        }).collect()
752    }
753
754    pub fn clear(&mut self) {
755        self.entries.clear();
756    }
757}
758
759// ─── Game Events ────────────────────────────────────────────────────────────────
760
761#[derive(Debug, Clone)]
762pub enum GameEvent {
763    PlayerSpawned { player_id: u32 },
764    PlayerDied { player_id: u32, cause: String },
765    PlayerLevelUp { player_id: u32, new_level: u32 },
766    PlayerTookDamage { player_id: u32, amount: f32, source: String },
767    PlayerHealed { player_id: u32, amount: f32 },
768    PlayerGainedXp { player_id: u32, amount: u32 },
769    EnemySpawned { enemy_id: u32, enemy_type: String },
770    EnemyDied { enemy_id: u32, enemy_type: String, killer_id: Option<u32> },
771    EnemyTookDamage { enemy_id: u32, amount: f32 },
772    ItemDropped { item_id: u32, item_type: String, position: (f32, f32) },
773    ItemPickedUp { item_id: u32, player_id: u32 },
774    ItemEquipped { item_id: u32, player_id: u32, slot: String },
775    ItemCrafted { recipe_id: String, player_id: u32 },
776    GoldChanged { amount: i64, new_total: u64 },
777    QuestStarted { quest_id: String },
778    QuestUpdated { quest_id: String, progress: u32, required: u32 },
779    QuestCompleted { quest_id: String, rewards: Vec<String> },
780    LevelLoaded { level_id: String },
781    LevelCompleted { level_id: String, stars: u8 },
782    BossEncountered { boss_id: String },
783    BossDefeated { boss_id: String, time_taken: f32 },
784    SecretFound { secret_id: String },
785    ChestOpened { chest_id: u32, loot: Vec<String> },
786    SkillUsed { skill_id: String, player_id: u32 },
787    SpellCast { spell_id: String, player_id: u32 },
788    ComboAchieved { count: u32, multiplier: f32 },
789    ScoreChanged { new_score: u64, delta: i64 },
790    AchievementUnlocked { achievement_id: String },
791    StateChanged { from: String, to: String },
792    SessionStarted { session_number: u32 },
793    SessionEnded { playtime: f64 },
794    SettingsChanged { key: String, value: String },
795    CutsceneStarted { id: String },
796    CutsceneEnded { id: String },
797    DialogueStarted { npc_id: String },
798    DialogueEnded { npc_id: String },
799    TutorialStep { step_id: String, completed: bool },
800}
801
802impl GameEvent {
803    pub fn kind_name(&self) -> &str {
804        match self {
805            GameEvent::PlayerSpawned { .. } => "PlayerSpawned",
806            GameEvent::PlayerDied { .. } => "PlayerDied",
807            GameEvent::PlayerLevelUp { .. } => "PlayerLevelUp",
808            GameEvent::PlayerTookDamage { .. } => "PlayerTookDamage",
809            GameEvent::PlayerHealed { .. } => "PlayerHealed",
810            GameEvent::PlayerGainedXp { .. } => "PlayerGainedXp",
811            GameEvent::EnemySpawned { .. } => "EnemySpawned",
812            GameEvent::EnemyDied { .. } => "EnemyDied",
813            GameEvent::EnemyTookDamage { .. } => "EnemyTookDamage",
814            GameEvent::ItemDropped { .. } => "ItemDropped",
815            GameEvent::ItemPickedUp { .. } => "ItemPickedUp",
816            GameEvent::ItemEquipped { .. } => "ItemEquipped",
817            GameEvent::ItemCrafted { .. } => "ItemCrafted",
818            GameEvent::GoldChanged { .. } => "GoldChanged",
819            GameEvent::QuestStarted { .. } => "QuestStarted",
820            GameEvent::QuestUpdated { .. } => "QuestUpdated",
821            GameEvent::QuestCompleted { .. } => "QuestCompleted",
822            GameEvent::LevelLoaded { .. } => "LevelLoaded",
823            GameEvent::LevelCompleted { .. } => "LevelCompleted",
824            GameEvent::BossEncountered { .. } => "BossEncountered",
825            GameEvent::BossDefeated { .. } => "BossDefeated",
826            GameEvent::SecretFound { .. } => "SecretFound",
827            GameEvent::ChestOpened { .. } => "ChestOpened",
828            GameEvent::SkillUsed { .. } => "SkillUsed",
829            GameEvent::SpellCast { .. } => "SpellCast",
830            GameEvent::ComboAchieved { .. } => "ComboAchieved",
831            GameEvent::ScoreChanged { .. } => "ScoreChanged",
832            GameEvent::AchievementUnlocked { .. } => "AchievementUnlocked",
833            GameEvent::StateChanged { .. } => "StateChanged",
834            GameEvent::SessionStarted { .. } => "SessionStarted",
835            GameEvent::SessionEnded { .. } => "SessionEnded",
836            GameEvent::SettingsChanged { .. } => "SettingsChanged",
837            GameEvent::CutsceneStarted { .. } => "CutsceneStarted",
838            GameEvent::CutsceneEnded { .. } => "CutsceneEnded",
839            GameEvent::DialogueStarted { .. } => "DialogueStarted",
840            GameEvent::DialogueEnded { .. } => "DialogueEnded",
841            GameEvent::TutorialStep { .. } => "TutorialStep",
842        }
843    }
844}
845
846// ─── Game Event Bus ─────────────────────────────────────────────────────────────
847
848type EventHandler = Box<dyn Fn(&GameEvent) + Send + Sync>;
849
850pub struct GameEventBus {
851    handlers: HashMap<String, Vec<EventHandler>>,
852    queue: VecDeque<GameEvent>,
853    history: Vec<GameEvent>,
854    history_limit: usize,
855}
856
857impl GameEventBus {
858    pub fn new() -> Self {
859        Self {
860            handlers: HashMap::new(),
861            queue: VecDeque::new(),
862            history: Vec::new(),
863            history_limit: 100,
864        }
865    }
866
867    pub fn with_history_limit(mut self, limit: usize) -> Self {
868        self.history_limit = limit;
869        self
870    }
871
872    pub fn subscribe(&mut self, kind: impl Into<String>, handler: EventHandler) {
873        self.handlers.entry(kind.into()).or_default().push(handler);
874    }
875
876    pub fn subscribe_all(&mut self, handler: EventHandler) {
877        self.handlers.entry("*".to_string()).or_default().push(handler);
878    }
879
880    pub fn publish(&mut self, event: GameEvent) {
881        self.queue.push_back(event);
882    }
883
884    pub fn publish_immediate(&mut self, event: GameEvent) {
885        let kind = event.kind_name().to_string();
886        if let Some(handlers) = self.handlers.get(&kind) {
887            for handler in handlers {
888                handler(&event);
889            }
890        }
891        if let Some(all_handlers) = self.handlers.get("*") {
892            for handler in all_handlers {
893                handler(&event);
894            }
895        }
896        if self.history.len() >= self.history_limit {
897            self.history.remove(0);
898        }
899        self.history.push(event);
900    }
901
902    pub fn flush(&mut self) {
903        while let Some(event) = self.queue.pop_front() {
904            self.publish_immediate(event);
905        }
906    }
907
908    pub fn clear_handlers(&mut self, kind: &str) {
909        self.handlers.remove(kind);
910    }
911
912    pub fn clear_all_handlers(&mut self) {
913        self.handlers.clear();
914    }
915
916    pub fn history(&self) -> &[GameEvent] {
917        &self.history
918    }
919
920    pub fn pending_count(&self) -> usize {
921        self.queue.len()
922    }
923}
924
925impl Default for GameEventBus {
926    fn default() -> Self {
927        Self::new()
928    }
929}
930
931// ─── Game Manager ───────────────────────────────────────────────────────────────
932
933pub struct GameManager {
934    pub state: GameState,
935    pub config: GameConfig,
936    pub timer: GameTimer,
937    pub session_stats: SessionStats,
938    pub score: Score,
939    pub combo: ComboTracker,
940    pub high_scores: HighScoreTable,
941    pub event_bus: GameEventBus,
942    pub transition: Option<GameTransition>,
943    pending_state: Option<GameState>,
944}
945
946impl GameManager {
947    pub fn new(config: GameConfig) -> Self {
948        Self {
949            state: GameState::MainMenu,
950            config,
951            timer: GameTimer::new(),
952            session_stats: SessionStats::new(),
953            score: Score::new(0),
954            combo: ComboTracker::new(),
955            high_scores: HighScoreTable::new(10),
956            event_bus: GameEventBus::new(),
957            transition: None,
958            pending_state: None,
959        }
960    }
961
962    pub fn tick(&mut self, dt: f32) {
963        self.timer.tick(dt as f64);
964        self.combo.tick(dt);
965        self.event_bus.flush();
966
967        if let Some(ref mut t) = self.transition {
968            t.tick(dt);
969            if t.complete {
970                if let Some(pending) = self.pending_state.take() {
971                    let old_name = self.state.name().to_string();
972                    let new_name = pending.name().to_string();
973                    self.state = pending;
974                    self.event_bus.publish(GameEvent::StateChanged {
975                        from: old_name,
976                        to: new_name,
977                    });
978                }
979                self.transition = None;
980            }
981        }
982
983        if let GameState::Playing = self.state {
984            self.session_stats.playtime_secs += dt as f64;
985        }
986    }
987
988    pub fn transition_to(&mut self, new_state: GameState, kind: TransitionKind, duration: f32) {
989        let from = self.state.name().to_string();
990        let to = new_state.name().to_string();
991        self.transition = Some(GameTransition::new(kind, duration, from, to));
992        self.pending_state = Some(new_state);
993    }
994
995    pub fn set_state(&mut self, new_state: GameState) {
996        let old_name = self.state.name().to_string();
997        let new_name = new_state.name().to_string();
998        self.state = new_state;
999        self.event_bus.publish(GameEvent::StateChanged {
1000            from: old_name,
1001            to: new_name,
1002        });
1003    }
1004
1005    pub fn start_game(&mut self) {
1006        self.session_stats = SessionStats::new();
1007        self.score = Score::new(0);
1008        self.combo = ComboTracker::new();
1009        self.timer.start_session();
1010        self.event_bus.publish(GameEvent::SessionStarted {
1011            session_number: self.timer.session_count,
1012        });
1013        self.transition_to(GameState::Playing, TransitionKind::Fade, 0.5);
1014    }
1015
1016    pub fn pause(&mut self) {
1017        if self.state.can_pause() {
1018            self.timer.pause();
1019            self.set_state(GameState::Paused);
1020        }
1021    }
1022
1023    pub fn resume(&mut self) {
1024        if matches!(self.state, GameState::Paused) {
1025            self.timer.resume();
1026            self.set_state(GameState::Playing);
1027        }
1028    }
1029
1030    pub fn game_over(&mut self, cause: impl Into<String>) {
1031        let data = GameOverData::new(
1032            self.score.total,
1033            cause,
1034            self.timer.elapsed,
1035            self.session_stats.enemies_killed,
1036            self.session_stats.max_level_reached,
1037        );
1038        let playtime = self.timer.elapsed;
1039        self.event_bus.publish(GameEvent::SessionEnded { playtime });
1040        self.high_scores.add(ScoreEntry::new("Player", self.score.total));
1041        self.transition_to(GameState::GameOver(data), TransitionKind::Fade, 1.0);
1042    }
1043
1044    pub fn add_score(&mut self, base: u64) {
1045        let with_combo = self.combo.apply_to_score(base);
1046        let delta = with_combo as i64;
1047        self.score.add_base(with_combo);
1048        self.event_bus.publish(GameEvent::ScoreChanged {
1049            new_score: self.score.total,
1050            delta,
1051        });
1052    }
1053
1054    pub fn record_hit(&mut self) -> f32 {
1055        let mult = self.combo.hit();
1056        let count = self.combo.count;
1057        if count > 1 {
1058            self.event_bus.publish(GameEvent::ComboAchieved {
1059                count,
1060                multiplier: mult,
1061            });
1062        }
1063        mult
1064    }
1065
1066    pub fn is_in_transition(&self) -> bool {
1067        self.transition.is_some()
1068    }
1069
1070    pub fn transition_progress(&self) -> f32 {
1071        self.transition.as_ref().map(|t| t.progress()).unwrap_or(1.0)
1072    }
1073
1074    pub fn current_state_name(&self) -> &str {
1075        self.state.name()
1076    }
1077}
1078
1079// ─── Tests ──────────────────────────────────────────────────────────────────────
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084
1085    #[test]
1086    fn test_load_progress_fraction() {
1087        let mut lp = LoadProgress::new("Textures", 4);
1088        assert_eq!(lp.fraction(), 0.0);
1089        lp.current = 2;
1090        lp.sub_progress = 0.5;
1091        assert!((lp.fraction() - 0.625).abs() < 1e-5);
1092        lp.current = 4;
1093        assert!(lp.is_complete());
1094    }
1095
1096    #[test]
1097    fn test_game_state_names() {
1098        assert_eq!(GameState::MainMenu.name(), "MainMenu");
1099        assert_eq!(GameState::Playing.name(), "Playing");
1100        assert_eq!(GameState::Paused.name(), "Paused");
1101        assert!(GameState::Playing.is_playing());
1102        assert!(GameState::Playing.can_pause());
1103        assert!(!GameState::Paused.can_pause());
1104    }
1105
1106    #[test]
1107    fn test_transition_progress() {
1108        let mut t = GameTransition::new(TransitionKind::Fade, 1.0, "MainMenu", "Playing");
1109        assert_eq!(t.progress(), 0.0);
1110        t.tick(0.5);
1111        assert!((t.progress() - 0.5).abs() < 1e-5);
1112        t.tick(0.5);
1113        assert!(t.complete);
1114    }
1115
1116    #[test]
1117    fn test_difficulty_params() {
1118        let p = DifficultyPreset::Hard.default_params();
1119        assert!(p.damage_scale > 1.0);
1120        assert!(p.enemy_health_scale > 1.0);
1121        let easy = DifficultyPreset::Easy.default_params();
1122        assert!(easy.damage_scale < 1.0);
1123    }
1124
1125    #[test]
1126    fn test_game_timer() {
1127        let mut timer = GameTimer::new();
1128        timer.tick(1.5);
1129        assert!((timer.elapsed - 1.5).abs() < 1e-9);
1130        timer.pause();
1131        timer.tick(1.0);
1132        assert!((timer.elapsed - 1.5).abs() < 1e-9);
1133        assert!((timer.paused_elapsed - 1.0).abs() < 1e-9);
1134        timer.resume();
1135        timer.tick(0.5);
1136        assert!((timer.elapsed - 2.0).abs() < 1e-9);
1137    }
1138
1139    #[test]
1140    fn test_combo_tracker() {
1141        let mut combo = ComboTracker::new();
1142        let m1 = combo.hit();
1143        assert!(m1 >= 1.0);
1144        let m2 = combo.hit();
1145        assert!(m2 >= m1);
1146        combo.tick(5.0); // beyond window
1147        assert!(!combo.is_active());
1148        assert_eq!(combo.count, 0);
1149    }
1150
1151    #[test]
1152    fn test_high_score_table() {
1153        let mut table = HighScoreTable::new(3);
1154        table.add(ScoreEntry::new("Alice", 1000));
1155        table.add(ScoreEntry::new("Bob", 5000));
1156        table.add(ScoreEntry::new("Carol", 3000));
1157        table.add(ScoreEntry::new("Dave", 500)); // should not displace anyone
1158        assert_eq!(table.entries.len(), 3);
1159        assert_eq!(table.entries[0].score, 5000);
1160        assert_eq!(table.entries[1].score, 3000);
1161    }
1162
1163    #[test]
1164    fn test_score_grades() {
1165        let mut score = Score::new(0);
1166        assert_eq!(score.grade(), 'F');
1167        score.add_base(50000);
1168        assert_eq!(score.grade(), 'S');
1169        score.add_base(50001);
1170        assert_eq!(score.grade(), 'X');
1171    }
1172
1173    #[test]
1174    fn test_event_bus_publish_immediate() {
1175        use std::sync::{Arc, Mutex};
1176        let received = Arc::new(Mutex::new(Vec::new()));
1177        let received_clone = received.clone();
1178        let mut bus = GameEventBus::new();
1179        bus.subscribe("ScoreChanged", Box::new(move |e| {
1180            received_clone.lock().unwrap().push(e.kind_name().to_string());
1181        }));
1182        bus.publish_immediate(GameEvent::ScoreChanged { new_score: 100, delta: 100 });
1183        assert_eq!(received.lock().unwrap().len(), 1);
1184    }
1185
1186    #[test]
1187    fn test_session_stats() {
1188        let mut stats = SessionStats::new();
1189        stats.record_kill(false);
1190        stats.record_kill(true);
1191        assert_eq!(stats.enemies_killed, 2);
1192        assert_eq!(stats.boss_kills, 1);
1193        stats.record_damage_dealt(150.0, true);
1194        assert_eq!(stats.critical_hits, 1);
1195        assert!((stats.damage_dealt - 150.0).abs() < 1e-9);
1196    }
1197
1198    #[test]
1199    fn test_game_manager_flow() {
1200        let config = GameConfig::default();
1201        let mut manager = GameManager::new(config);
1202        assert!(matches!(manager.state, GameState::MainMenu));
1203        // start_game triggers transition, so state is still MainMenu until tick
1204        manager.start_game();
1205        manager.tick(1.0); // complete the 0.5s transition
1206        assert!(matches!(manager.state, GameState::Playing));
1207        manager.pause();
1208        assert!(matches!(manager.state, GameState::Paused));
1209        manager.resume();
1210        assert!(matches!(manager.state, GameState::Playing));
1211    }
1212
1213    #[test]
1214    fn test_game_config_volume() {
1215        let mut cfg = GameConfig::default();
1216        cfg.master_volume = 0.5;
1217        cfg.music_volume = 0.8;
1218        let vol = cfg.effective_volume(VolumeChannel::Music);
1219        assert!((vol - 0.4).abs() < 1e-5);
1220    }
1221}