1pub 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#[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#[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#[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#[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); 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#[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#[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#[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#[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#[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#[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
856type 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
941pub 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#[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); 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)); 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 manager.start_game();
1215 manager.tick(1.0); 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}