diff --git a/DESIGN.md b/DESIGN.md index a3a25df..f60b3e5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -16,6 +16,7 @@ Eigenständiges Dungeon-Management-Spiel (Genre: Dungeon-Aufbau & Verteidigung), - Texturen: **prozedurale Pixelart** (Kachel-Atlas) - Figuren: **Pixel-Sprites als Billboards** (immer zur Kamera gedreht) - Licht: **Dunkel + Fackeln** – Grunddunkelheit, eroberte Bereiche und Lichtquellen (Fackeln, Kern, Portal, Tor) hell, flackerndes Licht +- **Kachel-Outlines**: dezente dunkle Linien um jedes Feld (Böden, Wandoberseiten, senkrechte Wandkanten), folgen der unregelmäßigen Oberfläche; abschaltbar (Taste G, später Grafik-Einstellungen) - UI bleibt 2D-Ebene darüber, Minikarte ## Steuerung diff --git a/src/main.rs b/src/main.rs index c4c6445..22db6c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,6 +101,9 @@ async fn main() { if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 { game.paused = !game.paused; } + if is_key_pressed(KeyCode::G) { + r3d.outlines = !r3d.outlines; + } if is_key_pressed(KeyCode::M) { sounds.muted = !sounds.muted; } diff --git a/src/render3d.rs b/src/render3d.rs index 20c7f97..8f5066c 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -319,6 +319,25 @@ impl MB { ); } } + // Thin strip lying on a surface (normal n) from p to q, used for tile outlines + fn strip(&mut self, p: Vec3, q: Vec3, n: Vec3, w: f32, c: [u8; 4]) { + let side = (q - p).cross(n).normalize_or_zero() * (w * 0.5); + let o = n * 0.006; + self.quad([p - side + o, q - side + o, q + side + o, p + side + o], [Vec2::ZERO; 4], [c; 4], [n; 4]); + } + // Polyline along displaced points + fn polyline(&mut self, pts: &[Vec3], n: Vec3, w: f32, c: [u8; 4]) { + for k in 0..pts.len().saturating_sub(1) { + self.strip(pts[k], pts[k + 1], n, w, c); + } + } + fn into_plain_mesh(self) -> Option { + if self.v.is_empty() { + None + } else { + Some(Mesh { vertices: self.v, indices: self.i, texture: None }) + } + } fn into_mesh(self, tex: &Texture2D) -> Option { if self.v.is_empty() { None @@ -379,9 +398,23 @@ fn solid_at(map: &Map, x: i32, z: i32) -> bool { map.get(x, z).map_or(true, |t| t.solid()) } -fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D) -> (Option, Option) { +const OUTLINE_W: f32 = 0.035; +const OUTLINE_C: [u8; 4] = [8, 5, 6, 150]; + +// Displaced points along a straight edge (same displacement as the wall grids -> lines follow the surface) +fn edge_pts(a: Vec3, b: Vec3, n: usize) -> Vec { + (0..=n) + .map(|k| { + let p = a.lerp(b, k as f32 / n as f32); + p + displace(p) + }) + .collect() +} + +fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D) -> (Option, Option, Option) { let mut mb = MB::new(); let mut ov = MB::new(); + let mut ln = MB::new(); let ld = LIGHT_DIR.normalize(); for z in cz * CHUNK..(cz + 1) * CHUNK { for x in cx * CHUNK..(cx + 1) * CHUNK { @@ -406,6 +439,13 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: c, [Vec3::Y; 4], ); + // outlines: north and west edge, only between two open tiles (each edge once) + if !solid_at(map, x, z - 1) { + ln.strip(vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf), Vec3::Y, OUTLINE_W, OUTLINE_C); + } + if !solid_at(map, x - 1, z) { + ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C); + } // room decor let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0); match t.kind { @@ -455,6 +495,19 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: (ua, ub), |_p, nrm| Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))), ); + // outlines on the wall top: north/west always, south/east where no wall continues + let tl = vec3(xf, WALL_H, zf); + let tr = vec3(xf + 1.0, WALL_H, zf); + let br = vec3(xf + 1.0, WALL_H, zf + 1.0); + let bl = vec3(xf, WALL_H, zf + 1.0); + ln.polyline(&edge_pts(tl, tr, n), Vec3::Y, OUTLINE_W, OUTLINE_C); + ln.polyline(&edge_pts(tl, bl, n), Vec3::Y, OUTLINE_W, OUTLINE_C); + if !solid_at(map, x, z + 1) { + ln.polyline(&edge_pts(bl, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C); + } + if !solid_at(map, x + 1, z) { + ln.polyline(&edge_pts(tr, br, n), Vec3::Y, OUTLINE_W, OUTLINE_C); + } if t.marked { let (a, b) = cell_uv(T_MARK); let y = WALL_H + 0.12; @@ -478,6 +531,10 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: _ => (vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0)), // west }; let out = vec3(dx as f32, 0.0, dz as f32); + // vertical outlines at both ends of the exposed face (tile borders and corners) + for end in [a, b] { + ln.polyline(&edge_pts(end, end + vec3(0.0, WALL_H, 0.0), n), out, OUTLINE_W, OUTLINE_C); + } build_grid( &mut mb, |u, v| a + (b - a) * u + vec3(0.0, WALL_H * (1.0 - v), 0.0), @@ -492,7 +549,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: } } } - (mb.into_mesh(atlas), ov.into_mesh(atlas)) + (mb.into_mesh(atlas), ov.into_mesh(atlas), ln.into_plain_mesh()) } // Subdivided, displaced face. pos(u, v) gives the undisplaced point (u, v in 0..1). @@ -542,6 +599,7 @@ struct Chunk { sig: u64, mesh: Option, overlay: Option, + lines: Option, } pub struct Overlay { @@ -556,6 +614,7 @@ pub struct R3d { lit: Material, glow: Material, chunks: Vec, + pub outlines: bool, // tile outlines on/off } fn rgba(u: u32) -> Color { @@ -585,8 +644,8 @@ 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 }).collect(); - R3d { lit: lit_material(), glow: glow_material(), chunks } + 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 } } fn update_chunks(&mut self, g: &Game, gfx: &Gfx) { @@ -599,9 +658,10 @@ 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) = 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); ch.mesh = m; ch.overlay = o; + ch.lines = l; ch.sig = sig; } } @@ -725,6 +785,11 @@ impl R3d { // ---- unlit glow / overlays gl_use_material(&self.glow); for c in &self.chunks { + if self.outlines { + if let Some(l) = &c.lines { + draw_mesh(l); + } + } if let Some(o) = &c.overlay { draw_mesh(o); } diff --git a/src/ui.rs b/src/ui.rs index 44eed49..2abd7af 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -424,7 +424,7 @@ impl Ui { ("Mausrad Zoomen", TXT), ("1-9, 0, X, V Werkzeug wählen", TXT), ("P / Leertaste Pause M Ton an/aus +/- Lautstärke", TXT), - ("H Diese Hilfe", TXT), + ("G Kachel-Umrisse an/aus H Diese Hilfe", TXT), ("", TXT), ("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT), ("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT),