From aae1d1a56a935b2aeba7eb953b71106b190da974 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 11 Jul 2026 01:35:02 +0200 Subject: animations, introduction, no terminal --- src/renderer.rs | 162 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 24 deletions(-) (limited to 'src/renderer.rs') diff --git a/src/renderer.rs b/src/renderer.rs index eec89b2..6eff9fa 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -38,14 +38,14 @@ const LETTERS: &[(&str, [u8; 5])] = &[ ("2", [0b01110, 0b10001, 0b00010, 0b00100, 0b11111]), ("3", [0b01110, 0b10001, 0b00110, 0b10001, 0b01110]), ("4", [0b00010, 0b00110, 0b01010, 0b11111, 0b00010]), - ("5", [0b11111, 0b10000, 0b11110, 0b10001, 0b01110]), + ("5", [0b11111, 0b10000, 0b11110, 0b00001, 0b11110]), ("6", [0b01110, 0b10000, 0b11110, 0b10001, 0b01110]), ("7", [0b11111, 0b00001, 0b00010, 0b00100, 0b00100]), ("8", [0b01110, 0b10001, 0b01110, 0b10001, 0b01110]), ("9", [0b01110, 0b10001, 0b01111, 0b00001, 0b01110]), // Punctuation & symbols ("!", [0b00100, 0b00100, 0b00100, 0b00000, 0b00100]), - ("@", [0b01110, 0b10001, 0b10111, 0b10000, 0b01110]), + ("@", [0b01110, 0b10001, 0b10110, 0b10000, 0b01110]), ("+", [0b00000, 0b00100, 0b01110, 0b00100, 0b00000]), (":", [0b00000, 0b00100, 0b00000, 0b00100, 0b00000]), ("%", [0b10001, 0b00010, 0b00100, 0b01000, 0b10001]), @@ -98,6 +98,8 @@ impl Renderer { let fov = std::f64::consts::PI / 3.0; // Per-column wall distances for entity occlusion. + // Store Euclidean (for occlusion) and perp (for rendering). + let mut wall_euclidean = vec![f64::MAX; self.screen_w]; let mut wall_dists = vec![f64::MAX; self.screen_w]; for sx in 0..self.screen_w { @@ -170,6 +172,7 @@ impl Renderer { } let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); + wall_euclidean[sx] = euclidean; wall_dists[sx] = perp_dist; let wall_h = (self.screen_h as f64 / perp_dist) as isize; @@ -230,7 +233,7 @@ impl Renderer { } // ---- entity sprites (drawn after walls so occlusion is easy) ---- - self.draw_entities(game, buffer, &wall_dists, fov, half_h); + self.draw_entities(game, buffer, &wall_euclidean, fov, half_h); // ---- camera bob ---- let bob = game.camera_bob().round() as isize; @@ -257,6 +260,33 @@ impl Renderer { // ---- HUD ---- self.draw_hud(buffer, game); + + // ---- controls overlay (shown on start) ---- + if game.show_controls { + self.draw_controls_overlay(buffer); + } + + // ---- screen flash ---- + if game.flash_alpha > 0.001 { + let r = ((game.flash_color >> 16) & 0xFF) as f64; + let g = ((game.flash_color >> 8) & 0xFF) as f64; + let b = (game.flash_color & 0xFF) as f64; + let a = game.flash_alpha; + for py in 0..self.screen_h { + for px in 0..self.screen_w { + let idx = py * self.screen_w + px; + let existing = buffer[idx]; + let er = ((existing >> 16) & 0xFF) as f64; + let eg = ((existing >> 8) & 0xFF) as f64; + let eb = (existing & 0xFF) as f64; + let fr = (er * (1.0 - a) + r * a) as u8; + let fg = (eg * (1.0 - a) + g * a) as u8; + let fb = (eb * (1.0 - a) + b * a) as u8; + buffer[idx] = + 0xFF00_0000 | ((fr as u32) << 16) | ((fg as u32) << 8) | (fb as u32); + } + } + } } // ---- entity sprites --------------------------------------------------- @@ -265,13 +295,25 @@ impl Renderer { &self, game: &Game, buffer: &mut [u32], - wall_dists: &[f64], + wall_euclidean: &[f64], fov: f64, half_h: f64, ) { let mw = game.maze.width(); let mh = game.maze.height(); + // Collect visible entities with their screen info. + struct Sprite { + sx: isize, + sy: isize, + scale: f64, + ch: char, + color: u32, + cam_z: f64, + } + + let mut sprites: Vec = Vec::new(); + for y in 0..mh { for x in 0..mw { let entity = match game.maze.entity_at(x, y) { @@ -290,14 +332,13 @@ impl Renderer { let rx = -fy; let ry = fx; - let cam_z = dx * fx + dy * fy; // depth + let cam_z = dx * fx + dy * fy; if cam_z <= 0.1 { - continue; // behind camera + continue; } let cam_x = dx * rx + dy * ry; - // Check if entity is within FOV. let half_fov_tan = (fov / 2.0).tan(); let screen_x_norm = cam_x / (cam_z * half_fov_tan); if screen_x_norm < -1.0 || screen_x_norm > 1.0 { @@ -306,35 +347,46 @@ impl Renderer { let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize; - // Entity height on screen. let entity_h = (self.screen_h as f64 / cam_z * 0.4) as isize; let sy = (half_h - entity_h as f64 * 0.3) as isize; - let (ch, color) = entity_char_and_color(entity); - - // Check occlusion: is any wall column this entity overlaps with closer? - let left = (sx - entity_h / 2).max(0) as usize; - let right = (sx + entity_h / 2).min(self.screen_w as isize - 1) as usize; - let visible = (left..=right).any(|col| cam_z < wall_dists[col]); - - if !visible { + // Occlusion: entity is visible only if its centre column is not behind a wall. + // Compare Euclidean distance to entity vs Euclidean distance to wall. + let col = sx.clamp(0, self.screen_w as isize - 1) as usize; + if cam_z >= wall_euclidean[col] { continue; } let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); + let (ch, color) = entity_char_and_color(entity); - self.draw_char_scaled( - buffer, - ch, + sprites.push(Sprite { sx, sy, scale, + ch, color, cam_z, - game.weakness_factor(), - ); + }); } } + + // Sort farthest first so nearby entities draw on top. + sprites.sort_by(|a, b| b.cam_z.partial_cmp(&a.cam_z).unwrap()); + + for s in &sprites { + self.draw_char_scaled( + buffer, + s.ch, + s.sx, + s.sy, + s.scale, + s.color, + s.cam_z, + game.weakness_factor(), + wall_euclidean, + ); + } } fn draw_char_scaled( @@ -345,8 +397,9 @@ impl Renderer { cy: isize, scale: f64, color: u32, - distance: f64, + cam_z: f64, weakness: f64, + wall_euclidean: &[f64], ) { let glyph = match find_glyph(ch) { Some(g) => g, @@ -354,7 +407,7 @@ impl Renderer { }; let darken_scale = 1.0 + weakness * 12.0; - let bright = 1.0 / (1.0 + distance * 0.15 * darken_scale); + let bright = 1.0 / (1.0 + cam_z * 0.15 * darken_scale); let bright_fx = (bright * 256.0) as u32; let final_color = darken_fx(color, bright_fx); @@ -377,6 +430,10 @@ impl Renderer { for py in py_start.max(0)..py_end.min(self.screen_h as isize) { for px in px_start.max(0)..px_end.min(self.screen_w as isize) { + // Per-column occlusion: skip if wall is closer. + if cam_z >= wall_euclidean[px as usize] { + continue; + } let idx = py as usize * self.screen_w + px as usize; buffer[idx] = final_color; } @@ -468,9 +525,66 @@ impl Renderer { } } } - x += LETTER_W + 1; // 1px gap + x += LETTER_W + 1; } } + + fn draw_controls_overlay(&self, buffer: &mut [u32]) { + // Semi-transparent darken. + for py in 0..self.screen_h { + for px in 0..self.screen_w { + let idx = py * self.screen_w + px; + let c = buffer[idx]; + let r = ((c >> 16) & 0xFF) as u32; + let g = ((c >> 8) & 0xFF) as u32; + let b = (c & 0xFF) as u32; + buffer[idx] = 0xFF00_0000 | ((r / 3) << 16) | ((g / 3) << 8) | (b / 3); + } + } + + let title_color = 0xFF_CC8844; + let key_color = 0xFF_88CC88; + let desc_color = 0xFF_CCCCCC; + let hint_color = 0xFF_888888; + + let cx = self.screen_w / 2; + let mut y = 40usize; + + // Title. + let title = "DUNGEON CRAWLER"; + let title_w = title.len() * (LETTER_W + 1) - 1; + self.draw_text(buffer, cx - title_w / 2, y, title, title_color); + y += LETTER_H + 10; + + // Controls. + let lines: &[(&str, &str)] = &[ + ("WASD", "MOVE / STRAFE"), + ("Q / E", "TURN 90 LEFT / RIGHT"), + ("SPACE", "ATTACK"), + ("Z", "USE SELECTED ITEM"), + ("X", "DISCARD SELECTED ITEM"), + ("1 - 4", "SELECT INVENTORY SLOT"), + ("C", "SKIP TURN"), + ("ESC", "QUIT"), + ]; + + for (key, desc) in lines { + let key_px = key.len() * (LETTER_W + 1); + let gap_px = 2 * (LETTER_W + 1); + let desc_px = desc.len() * (LETTER_W + 1); + let total_w = key_px + gap_px + desc_px; + let start_x = cx - total_w / 2; + + self.draw_text(buffer, start_x, y, key, key_color); + self.draw_text(buffer, start_x + key_px + gap_px, y, desc, desc_color); + y += LETTER_H + 4; + } + + y += 12; + let hint = "PRESS ANY KEY TO BEGIN"; + let hint_w = hint.len() * (LETTER_W + 1) - 1; + self.draw_text(buffer, cx - hint_w / 2, y, hint, hint_color); + } } // ---- helpers ---- -- cgit v1.2.3