1pub mod menu;
7pub mod localization;
8pub mod achievements;
9
10use std::collections::{HashMap, VecDeque};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[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#[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#[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#[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); 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#[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#[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#[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#[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#[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#[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
846type 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
931pub 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#[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); 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)); 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 manager.start_game();
1205 manager.tick(1.0); 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}