use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
const SETTINGS_FILE: &str = "settings.ron";
pub const TEXT_SPEED_MIN: f32 = 0.5;
pub const TEXT_SPEED_MAX: f32 = 2.0;
pub const TEXT_SPEED_DEFAULT: f32 = 1.0;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameSettings {
pub text_speed_multiplier: f32,
}
impl Default for GameSettings {
fn default() -> Self {
Self {
text_speed_multiplier: TEXT_SPEED_DEFAULT,
}
}
}
impl GameSettings {
pub fn validate(&mut self) {
self.text_speed_multiplier = self.text_speed_multiplier.clamp(TEXT_SPEED_MIN, TEXT_SPEED_MAX);
}
pub fn load(save_dir: &Path) -> Self {
let path = save_dir.join(SETTINGS_FILE);
if !path.exists() {
return Self::default();
}
match std::fs::read_to_string(&path) {
Ok(contents) => {
match ron::from_str::<GameSettings>(&contents) {
Ok(mut settings) => {
settings.validate();
settings
}
Err(_) => Self::default(),
}
}
Err(_) => Self::default(),
}
}
pub fn save(&self, save_dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(save_dir)
.with_context(|| format!("failed to create save directory: {}", save_dir.display()))?;
let path = save_dir.join(SETTINGS_FILE);
let serialized = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.context("failed to serialize settings")?;
std::fs::write(&path, &serialized)
.with_context(|| format!("failed to write settings: {}", path.display()))?;
Ok(path)
}
pub fn apply_text_speed(&self, base_ms: u64) -> u64 {
if base_ms == 0 {
return 0; }
let adjusted = base_ms as f64 / self.text_speed_multiplier as f64;
(adjusted.round() as u64).max(1) }
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn default_settings() {
let s = GameSettings::default();
assert!((s.text_speed_multiplier - 1.0).abs() < f32::EPSILON);
}
#[test]
fn validate_clamps_low() {
let mut s = GameSettings { text_speed_multiplier: 0.1 };
s.validate();
assert!((s.text_speed_multiplier - TEXT_SPEED_MIN).abs() < f32::EPSILON);
}
#[test]
fn validate_clamps_high() {
let mut s = GameSettings { text_speed_multiplier: 10.0 };
s.validate();
assert!((s.text_speed_multiplier - TEXT_SPEED_MAX).abs() < f32::EPSILON);
}
#[test]
fn round_trip() {
let dir = TempDir::new().unwrap();
let settings = GameSettings { text_speed_multiplier: 1.5 };
settings.save(dir.path()).unwrap();
let loaded = GameSettings::load(dir.path());
assert!((loaded.text_speed_multiplier - 1.5).abs() < f32::EPSILON);
}
#[test]
fn load_missing_returns_default() {
let dir = TempDir::new().unwrap();
let loaded = GameSettings::load(dir.path());
assert!((loaded.text_speed_multiplier - TEXT_SPEED_DEFAULT).abs() < f32::EPSILON);
}
#[test]
fn load_corrupt_returns_default() {
let dir = TempDir::new().unwrap();
let path = dir.path().join(SETTINGS_FILE);
std::fs::write(&path, "this is not valid RON").unwrap();
let loaded = GameSettings::load(dir.path());
assert!((loaded.text_speed_multiplier - TEXT_SPEED_DEFAULT).abs() < f32::EPSILON);
}
#[test]
fn apply_text_speed_multiplier() {
let s = GameSettings { text_speed_multiplier: 2.0 };
assert_eq!(s.apply_text_speed(30), 15);
let slow = GameSettings { text_speed_multiplier: 0.5 };
assert_eq!(slow.apply_text_speed(30), 60);
}
#[test]
fn apply_text_speed_crisis_stays_zero() {
let s = GameSettings { text_speed_multiplier: 2.0 };
assert_eq!(s.apply_text_speed(0), 0);
}
#[test]
fn apply_text_speed_minimum_one() {
let fast = GameSettings { text_speed_multiplier: 2.0 };
assert!(fast.apply_text_speed(1) >= 1);
}
}