Title screen, pause menu, settings dialog and savegames
- title screen (continue/new/load/settings/help) and Esc pause menu - settings: language & sound, camera, graphics, game, rebindable keys; stored in localStorage and in every savegame (always applied on load) - savegames as JSON (.dcsave): download, load via file picker or drag & drop - autosave to localStorage every 2 minutes and when the tab is hidden/closed - configurable wall detail, light count, interface size, game speed, hints - audio behind a cargo feature so native tests run (cargo test --no-default-features) - savegame round-trip test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbf6f223fc
commit
fb7dab3683
17 files changed
+1742
-345
No files matched your search
+90
@@ -0,0 +1,90 @@
|
||||
// Savegames: JSON with format tag and version, containing the game state, the camera
|
||||
// and the player's settings (settings are always applied when a savegame is loaded).
|
||||
|
||||
use crate::game::Game;
|
||||
use crate::render3d::Cam;
|
||||
use crate::settings::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const FORMAT: &str = "dungeoncraft-save";
|
||||
pub const VERSION: u32 = 1;
|
||||
pub const AUTOSAVE_KEY: &str = "dc.autosave";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SaveOut<'a> {
|
||||
format: &'a str,
|
||||
version: u32,
|
||||
game: &'a Game,
|
||||
camera: &'a Cam,
|
||||
settings: &'a Settings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SaveIn {
|
||||
format: String,
|
||||
version: u32,
|
||||
game: Game,
|
||||
camera: Option<Cam>,
|
||||
settings: Option<Settings>,
|
||||
}
|
||||
|
||||
pub struct Loaded {
|
||||
pub game: Game,
|
||||
pub camera: Option<Cam>,
|
||||
pub settings: Option<Settings>,
|
||||
}
|
||||
|
||||
pub fn to_json(game: &Game, camera: &Cam, settings: &Settings) -> String {
|
||||
serde_json::to_string(&SaveOut { format: FORMAT, version: VERSION, game, camera, settings }).unwrap_or_default()
|
||||
}
|
||||
|
||||
// Err contains a language key describing the problem
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Loaded, &'static str> {
|
||||
let s: SaveIn = serde_json::from_slice(data).map_err(|_| "save.err_invalid")?;
|
||||
if s.format != FORMAT {
|
||||
return Err("save.err_invalid");
|
||||
}
|
||||
if s.version > VERSION {
|
||||
return Err("save.err_version");
|
||||
}
|
||||
let mut game = s.game;
|
||||
game.recount();
|
||||
Ok(Loaded { game, camera: s.camera, settings: s.settings })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn save_roundtrip() {
|
||||
let mut g = Game::new(42);
|
||||
let (cx, cy) = g.map.core;
|
||||
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
||||
for _ in 0..3000 {
|
||||
g.update(0.05);
|
||||
}
|
||||
let cam = Cam::new(10.0, 20.0);
|
||||
let mut settings = Settings::default();
|
||||
settings.language = "de".into();
|
||||
let json = to_json(&g, &cam, &settings);
|
||||
println!("savegame size: {} KB", json.len() / 1024);
|
||||
let l = from_bytes(json.as_bytes()).expect("load");
|
||||
assert_eq!(l.game.time, g.time);
|
||||
assert_eq!(l.game.ents.len(), g.ents.len());
|
||||
assert_eq!(l.game.gold, g.gold);
|
||||
assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.kind == b.kind && a.owner == b.owner && a.marked == b.marked));
|
||||
assert_eq!(l.settings.unwrap().language, "de");
|
||||
assert_eq!(l.camera.unwrap().tx, 10.0);
|
||||
// the loaded game keeps running identically
|
||||
let mut a = l.game;
|
||||
let mut b = g;
|
||||
for _ in 0..2000 {
|
||||
a.update(0.05);
|
||||
b.update(0.05);
|
||||
}
|
||||
assert_eq!(a.gold, b.gold);
|
||||
assert_eq!(a.ents.len(), b.ents.len());
|
||||
assert!(from_bytes(b"{\"format\":\"x\"}").is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user