//! Game configuration, loaded from `./game.ini`. //! //! Two settings today: //! - `typewriter_effect` (bool, default `true`) — animate level prompts //! character-by-character as they appear in the log. //! - `typewriter_speed_cpm` (u32, default `1000`) — typing speed in //! characters per minute (~60 ms per character). //! //! Missing keys fall back to defaults. Missing file falls back to all //! defaults. Comments (`#` or `;`) and `[sections]` are ignored. use std::path::PathBuf; use std::time::Duration; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct GameConfig { pub typewriter_enabled: bool, pub typewriter_speed_cpm: u32, } impl Default for GameConfig { fn default() -> Self { Self { typewriter_enabled: true, typewriter_speed_cpm: 1000, } } } impl GameConfig { /// How long each character takes to appear during the typewriter /// animation. Falls back to the default speed if `speed_cpm` is 0. pub fn char_duration(&self) -> Duration { let cpm = if self.typewriter_speed_cpm == 0 { Self::default().typewriter_speed_cpm } else { self.typewriter_speed_cpm }; Duration::from_millis(60_000 / cpm as u64) } } pub fn config_path() -> PathBuf { PathBuf::from("game.ini") } pub fn load() -> GameConfig { match std::fs::read_to_string(config_path()) { Ok(content) => parse(&content), Err(_) => GameConfig::default(), } } pub fn parse(content: &str) -> GameConfig { let mut cfg = GameConfig::default(); for line in content.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') || line.starts_with(';') || line.starts_with('[') { continue; } let Some((key, value)) = line.split_once('=') else { continue; }; let key = key.trim().to_lowercase().replace(' ', "_"); let value = value.trim(); match key.as_str() { "typewriter_effect" | "typewriter_enabled" => { if let Ok(b) = value.parse::() { cfg.typewriter_enabled = b; } } "typewriter_speed" | "typewriter_speed_cpm" => { if let Ok(n) = value.parse::() { cfg.typewriter_speed_cpm = n; } } _ => {} } } cfg } #[cfg(test)] mod tests { use super::*; #[test] fn defaults() { let cfg = GameConfig::default(); assert!(cfg.typewriter_enabled); assert_eq!(cfg.typewriter_speed_cpm, 1000); // 1000 cpm = 60 ms per char assert_eq!(cfg.char_duration().as_millis(), 60); } #[test] fn parser_reads_both_underscored_and_spaced_keys() { let ini = "\ # comment\n\ ; another comment\n\ [section]\n\ typewriter effect = false\n\ typewriter_speed_cpm = 240\n\ "; let cfg = parse(ini); assert!(!cfg.typewriter_enabled); assert_eq!(cfg.typewriter_speed_cpm, 240); // 240 cpm = 250 ms per char assert_eq!(cfg.char_duration().as_millis(), 250); } #[test] fn parser_keeps_defaults_on_garbage() { let cfg = parse("typewriter_effect = maybe\ntypewriter_speed_cpm = fast\n"); assert_eq!(cfg, GameConfig::default()); } #[test] fn zero_speed_falls_back_to_default_duration() { let cfg = GameConfig { typewriter_enabled: true, typewriter_speed_cpm: 0, }; assert_eq!(cfg.char_duration().as_millis(), 60); } }