Tile outlines on floors, wall tops and wall corners (toggle with G)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-02 22:45:36 +01:00
1 parent 220d9543cc
commit a101f6ddc4
4 files changed
+75 -6

No files matched your search

+1
View File
@@ -16,6 +16,7 @@ Eigenständiges Dungeon-Management-Spiel (Genre: Dungeon-Aufbau & Verteidigung),
- Texturen: **prozedurale Pixelart** (Kachel-Atlas) - Texturen: **prozedurale Pixelart** (Kachel-Atlas)
- Figuren: **Pixel-Sprites als Billboards** (immer zur Kamera gedreht) - Figuren: **Pixel-Sprites als Billboards** (immer zur Kamera gedreht)
- Licht: **Dunkel + Fackeln** – Grunddunkelheit, eroberte Bereiche und Lichtquellen (Fackeln, Kern, Portal, Tor) hell, flackerndes Licht - 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 - UI bleibt 2D-Ebene darüber, Minikarte
## Steuerung ## Steuerung
+3
View File
@@ -101,6 +101,9 @@ async fn main() {
if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 { if (is_key_pressed(KeyCode::P) || is_key_pressed(KeyCode::Space)) && game.state == 0 {
game.paused = !game.paused; game.paused = !game.paused;
} }
if is_key_pressed(KeyCode::G) {
r3d.outlines = !r3d.outlines;
}
if is_key_pressed(KeyCode::M) { if is_key_pressed(KeyCode::M) {
sounds.muted = !sounds.muted; sounds.muted = !sounds.muted;
} }
+70 -5
View File
@@ -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<Mesh> {
if self.v.is_empty() {
None
} else {
Some(Mesh { vertices: self.v, indices: self.i, texture: None })
}
}
fn into_mesh(self, tex: &Texture2D) -> Option<Mesh> { fn into_mesh(self, tex: &Texture2D) -> Option<Mesh> {
if self.v.is_empty() { if self.v.is_empty() {
None 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()) 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<Mesh>, Option<Mesh>) { 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<Vec3> {
(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<Mesh>, Option<Mesh>, Option<Mesh>) {
let mut mb = MB::new(); let mut mb = MB::new();
let mut ov = MB::new(); let mut ov = MB::new();
let mut ln = MB::new();
let ld = LIGHT_DIR.normalize(); let ld = LIGHT_DIR.normalize();
for z in cz * CHUNK..(cz + 1) * CHUNK { for z in cz * CHUNK..(cz + 1) * CHUNK {
for x in cx * CHUNK..(cx + 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, c,
[Vec3::Y; 4], [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 // room decor
let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0); let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0);
match t.kind { match t.kind {
@@ -455,6 +495,19 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
(ua, ub), (ua, ub),
|_p, nrm| Vec3::splat(top_amb * (0.75 + 0.25 * nrm.dot(ld).max(0.0))), |_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 { if t.marked {
let (a, b) = cell_uv(T_MARK); let (a, b) = cell_uv(T_MARK);
let y = WALL_H + 0.12; 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 _ => (vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0)), // west
}; };
let out = vec3(dx as f32, 0.0, dz as f32); 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( build_grid(
&mut mb, &mut mb,
|u, v| a + (b - a) * u + vec3(0.0, WALL_H * (1.0 - v), 0.0), |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). // Subdivided, displaced face. pos(u, v) gives the undisplaced point (u, v in 0..1).
@@ -542,6 +599,7 @@ struct Chunk {
sig: u64, sig: u64,
mesh: Option<Mesh>, mesh: Option<Mesh>,
overlay: Option<Mesh>, overlay: Option<Mesh>,
lines: Option<Mesh>,
} }
pub struct Overlay { pub struct Overlay {
@@ -556,6 +614,7 @@ pub struct R3d {
lit: Material, lit: Material,
glow: Material, glow: Material,
chunks: Vec<Chunk>, chunks: Vec<Chunk>,
pub outlines: bool, // tile outlines on/off
} }
fn rgba(u: u32) -> Color { fn rgba(u: u32) -> Color {
@@ -585,8 +644,8 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 {
impl R3d { impl R3d {
pub fn new() -> R3d { pub fn new() -> R3d {
let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None }).collect(); let chunks = (0..NCH * NCH).map(|_| Chunk { sig: 0, mesh: None, overlay: None, lines: None }).collect();
R3d { lit: lit_material(), glow: glow_material(), chunks } R3d { lit: lit_material(), glow: glow_material(), chunks, outlines: true }
} }
fn update_chunks(&mut self, g: &Game, gfx: &Gfx) { 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]; let ch = &mut self.chunks[(cz * NCH + cx) as usize];
if ch.sig != sig { if ch.sig != sig {
let a = amb.get_or_insert_with(|| ambient_field(&g.map)); 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.mesh = m;
ch.overlay = o; ch.overlay = o;
ch.lines = l;
ch.sig = sig; ch.sig = sig;
} }
} }
@@ -725,6 +785,11 @@ impl R3d {
// ---- unlit glow / overlays // ---- unlit glow / overlays
gl_use_material(&self.glow); gl_use_material(&self.glow);
for c in &self.chunks { for c in &self.chunks {
if self.outlines {
if let Some(l) = &c.lines {
draw_mesh(l);
}
}
if let Some(o) = &c.overlay { if let Some(o) = &c.overlay {
draw_mesh(o); draw_mesh(o);
} }
+1 -1
View File
@@ -424,7 +424,7 @@ impl Ui {
("Mausrad Zoomen", TXT), ("Mausrad Zoomen", TXT),
("1-9, 0, X, V Werkzeug wählen", TXT), ("1-9, 0, X, V Werkzeug wählen", TXT),
("P / Leertaste Pause M Ton an/aus +/- Lautstärke", 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), ("", TXT),
("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT), ("1. Grabe zum Portal und lass es von Grubenwichten erobern.", TXT),
("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT), ("2. Baue Quartier, Pilzgarten und Schatzkammer.", TXT),