Configurable map size (64-256, non-square), one portal per 64x64 tiles, guarded treasure caves, glowing dig-mark blocks

- New Game dialog with map width/height (steps of 32), remembered in settings
- map dimensions are per game; renderer chunks, minimap and camera follow them
- content scales with area; grace period grows with the distance to the Hero Gate
- treasure caves with rich gold, guarded by hero squads that hold their post
- marked blocks: translucent yellow box with outline on all edges
- savegame v2: packed run-length encoded tiles (256x256 map ~320 KB), v1 still loads
- tests: savegame round trip on 96x128, 256x256 generation/simulation/save

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 06:34:09 +01:00
1 parent 925be17e82
commit f710a19351
12 files changed
+588 -137

No files matched your search

+26 -2
View File
@@ -7,7 +7,7 @@ use crate::settings::Settings;
use serde::{Deserialize, Serialize};
pub const FORMAT: &str = "dungeoncraft-save";
pub const VERSION: u32 = 1;
pub const VERSION: u32 = 2; // 2: variable map size, several portals, packed tiles
pub const AUTOSAVE_KEY: &str = "dc.autosave";
#[derive(Serialize)]
@@ -48,6 +48,7 @@ pub fn from_bytes(data: &[u8]) -> Result<Loaded, &'static str> {
return Err("save.err_version");
}
let mut game = s.game;
game.map.activate();
game.recount();
Ok(Loaded { game, camera: s.camera, settings: s.settings })
}
@@ -58,7 +59,7 @@ mod tests {
#[test]
fn save_roundtrip() {
let mut g = Game::new(42);
let mut g = Game::new(42, 96, 128);
let (cx, cy) = g.map.core;
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
for _ in 0..3000 {
@@ -76,6 +77,9 @@ mod tests {
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);
assert_eq!((l.game.map.w, l.game.map.h), (96, 128));
assert_eq!(l.game.map.portals.len(), g.map.portals.len());
assert!(l.game.map.t.iter().zip(g.map.t.iter()).all(|(a, b)| a.var == b.var && a.hp == b.hp && a.food == b.food && a.gold == b.gold));
// the loaded game keeps running identically
let mut a = l.game;
let mut b = g;
@@ -87,4 +91,24 @@ mod tests {
assert_eq!(a.ents.len(), b.ents.len());
assert!(from_bytes(b"{\"format\":\"x\"}").is_err());
}
#[test]
fn huge_map() {
let t0 = std::time::Instant::now();
let mut g = Game::new(7, 256, 256);
let gen = t0.elapsed();
assert_eq!(g.map.portals.len(), 16);
assert_eq!(g.map.treasures.len(), 15);
let (cx, cy) = g.map.core;
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
let t1 = std::time::Instant::now();
for _ in 0..(120 * 20) {
g.update(0.05);
}
let sim = t1.elapsed();
let json = to_json(&g, &Cam::new(0.0, 0.0), &Settings::default());
println!("256x256: generate {:?}, 2 min simulated in {:?}, savegame {} KB", gen, sim, json.len() / 1024);
let l = from_bytes(json.as_bytes()).expect("load");
assert_eq!(l.game.map.t.len(), 256 * 256);
}
}