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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 23:08:33 +01:00
1 parent fbf6f223fc
commit fb7dab3683
17 files changed
+1742 -345

No files matched your search

+23 -6
View File
@@ -8,6 +8,7 @@ use crate::map::*;
use macroquad::miniquad::{BlendFactor, BlendState, BlendValue, Comparison, Equation, PipelineParams, UniformDesc, UniformType};
use macroquad::models::{Mesh, Vertex};
use macroquad::prelude::*;
use serde::{Deserialize, Serialize};
pub const WALL_H: f32 = 0.95;
const CHUNK: i32 = 16;
@@ -15,11 +16,11 @@ const NCH: i32 = MW as i32 / CHUNK;
pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0;
const ATLAS_H: f32 = 80.0;
const WALL_SUB: usize = 3; // subdivisions per wall face edge
const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera
#[derive(Clone, Serialize, Deserialize)]
pub struct Cam {
pub tx: f32, // look-at target on the ground (world x)
pub tz: f32, // look-at target on the ground (world z = map y)
@@ -411,7 +412,7 @@ fn edge_pts(a: Vec3, b: Vec3, n: usize) -> Vec<Vec3> {
.collect()
}
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D) -> (Option<Mesh>, Option<Mesh>, Option<Mesh>) {
fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> (Option<Mesh>, Option<Mesh>, Option<Mesh>) {
let mut mb = MB::new();
let mut ov = MB::new();
let mut ln = MB::new();
@@ -485,7 +486,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
[dark; 4],
[Vec3::Y; 4],
);
let n = WALL_SUB;
let n = wall_sub;
let top_amb = amb_at(amb, xf + 0.5, zf + 0.5) * 0.95 + 0.1;
build_grid(
&mut mb,
@@ -615,6 +616,8 @@ pub struct R3d {
glow: Material,
chunks: Vec<Chunk>,
pub outlines: bool, // tile outlines on/off
wall_sub: usize, // wall face subdivisions (detail)
pub max_lights: usize,
}
fn rgba(u: u32) -> Color {
@@ -645,7 +648,21 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
impl R3d {
pub fn new() -> R3d {
let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true }
R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS }
}
// Changing the wall detail rebuilds all chunks
pub fn set_wall_detail(&mut self, sub: usize) {
if sub != self.wall_sub {
self.wall_sub = sub;
self.invalidate();
}
}
pub fn invalidate(&mut self) {
for c in self.chunks.iter_mut() {
c.sig = 0;
}
}
fn update_chunks(&mut self, g: &Game, gfx: &Gfx) {
@@ -658,7 +675,7 @@ impl R3d {
let ch = &mut self.chunks[(cz * NCH + cx) as usize];
if ch.sig != sig {
let a = amb.get_or_insert_with(|| ambient_field(&g.map));
let (m, o, l) = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas);
let (m, o, l) = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub);
ch.mesh = m;
ch.overlay = o;
ch.lines = l;
@@ -714,7 +731,7 @@ impl R3d {
}
}
l.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
l.truncate(MAX_LIGHTS);
l.truncate(self.max_lights.min(MAX_LIGHTS));
let mut pos = vec![Vec4::ZERO; MAX_LIGHTS];
let mut colr = vec![Vec4::ZERO; MAX_LIGHTS];
for (k, (_, p, c)) in l.iter().enumerate() {