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