feat(persistence): add high score saving/loading

Add HighScore resource, persistence module with save/load, and
integration tests. Dependencies: serde, serde_json, dirs.
This commit is contained in:
Harald Hoyer 2026-05-07 23:30:59 +02:00
parent 459e8a2353
commit 060a9a2a14
6 changed files with 189 additions and 3 deletions

View file

@ -86,6 +86,9 @@ pub struct StartButton;
#[derive(Component)]
pub struct RestartMessage;
#[derive(Component)]
pub struct HighScoreText;
#[derive(Component)]
pub struct Star {
pub speed: f32,

73
src/persistence.rs Normal file
View file

@ -0,0 +1,73 @@
use std::path::{Path, PathBuf};
use crate::resources::HighScore;
pub fn save_path() -> PathBuf {
if let Some(p) = SAVE_PATH_OVERRIDE.get() {
return p.clone();
}
if let Ok(env_path) = std::env::var("BGLGA_HIGHSCORE_PATH") {
return PathBuf::from(env_path);
}
dirs::data_dir()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
.join("bglga")
.join("highscore.json")
}
pub fn save(score: &HighScore, path: &Path) -> Result<(), std::io::Error> {
std::fs::create_dir_all(path.parent().unwrap_or(Path::new(".")))?;
let content = serde_json::to_string_pretty(score)?;
std::fs::write(path, content)
}
pub fn load(path: &Path) -> HighScore {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return HighScore { value: 0 },
};
serde_json::from_str::<HighScore>(&content).unwrap_or(HighScore { value: 0 })
}
static SAVE_PATH_OVERRIDE: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
pub fn set_save_path_override(path: PathBuf) {
SAVE_PATH_OVERRIDE.set(path).ok();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_and_load_round_trip() {
let temp_path = std::env::temp_dir().join("highscore_roundtrip.json");
let score = HighScore { value: 42 };
save(&score, &temp_path).expect("save should succeed");
let loaded = load(&temp_path);
assert_eq!(loaded.value, 42, "loaded score should match saved score");
let _ = std::fs::remove_file(&temp_path);
}
#[test]
fn load_missing_file_returns_default() {
let missing_path = std::env::temp_dir().join("highscore_nonexistent.json");
let loaded = load(&missing_path);
assert_eq!(
loaded.value, 0,
"missing file should return default HighScore"
);
}
#[test]
fn load_malformed_json_returns_default() {
let temp_path = std::env::temp_dir().join("highscore_malformed.json");
std::fs::write(&temp_path, "{bad json").expect("write temp file should succeed");
let loaded = load(&temp_path);
assert_eq!(
loaded.value, 0,
"malformed JSON should return default HighScore"
);
let _ = std::fs::remove_file(&temp_path);
}
}

View file

@ -167,3 +167,9 @@ pub struct AttackDiveTimer {
pub struct RestartPressed {
pub pressed: bool,
}
#[derive(Resource, Default, serde::Serialize, serde::Deserialize)]
pub struct HighScore {
#[serde(rename = "high_score")]
pub value: u32,
}