From c8eae871002b57f45b29a2d158e384433e76a90c Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 10 Jul 2026 21:16:59 +0200 Subject: Phase 2 --- src/renderer.rs | 404 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 370 insertions(+), 34 deletions(-) (limited to 'src/renderer.rs') diff --git a/src/renderer.rs b/src/renderer.rs index bce3391..eec89b2 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,44 +1,104 @@ -use crate::game::Game; +use crate::game::{Entity, Game, PickupType}; use crate::texture::TEX_SIZE; +// ---- simple 5×5 ASCII glyphs ---- +// ---- simple 5×5 ASCII glyphs for letters & symbols (incomplete — add as needed) ---- + +const LETTERS: &[(&str, [u8; 5])] = &[ + // Uppercase A-Z + ("A", [0b01110, 0b10001, 0b11111, 0b10001, 0b10001]), + ("B", [0b11110, 0b10001, 0b11110, 0b10001, 0b11110]), + ("C", [0b01110, 0b10001, 0b10000, 0b10001, 0b01110]), + ("D", [0b11110, 0b10001, 0b10001, 0b10001, 0b11110]), + ("E", [0b11111, 0b10000, 0b11110, 0b10000, 0b11111]), + ("F", [0b11111, 0b10000, 0b11110, 0b10000, 0b10000]), + ("G", [0b01110, 0b10000, 0b10111, 0b10001, 0b01110]), + ("H", [0b10001, 0b10001, 0b11111, 0b10001, 0b10001]), + ("I", [0b01110, 0b00100, 0b00100, 0b00100, 0b01110]), + ("J", [0b00011, 0b00001, 0b00001, 0b10001, 0b01110]), + ("K", [0b10001, 0b10010, 0b11100, 0b10010, 0b10001]), + ("L", [0b10000, 0b10000, 0b10000, 0b10000, 0b11111]), + ("M", [0b10001, 0b11011, 0b10101, 0b10001, 0b10001]), + ("N", [0b10001, 0b11001, 0b10101, 0b10011, 0b10001]), + ("O", [0b01110, 0b10001, 0b10001, 0b10001, 0b01110]), + ("P", [0b11110, 0b10001, 0b11110, 0b10000, 0b10000]), + ("Q", [0b01110, 0b10001, 0b10001, 0b10010, 0b01101]), + ("R", [0b11110, 0b10001, 0b11110, 0b10010, 0b10001]), + ("S", [0b01110, 0b10000, 0b01110, 0b00001, 0b11110]), + ("T", [0b11111, 0b00100, 0b00100, 0b00100, 0b00100]), + ("U", [0b10001, 0b10001, 0b10001, 0b10001, 0b01110]), + ("V", [0b10001, 0b10001, 0b10001, 0b01010, 0b00100]), + ("W", [0b10001, 0b10001, 0b10101, 0b10101, 0b01010]), + ("X", [0b10001, 0b01010, 0b00100, 0b01010, 0b10001]), + ("Y", [0b10001, 0b01010, 0b00100, 0b00100, 0b00100]), + ("Z", [0b11111, 0b00010, 0b00100, 0b01000, 0b11111]), + // Digits + ("0", [0b01110, 0b10011, 0b10101, 0b11001, 0b01110]), + ("1", [0b00100, 0b01100, 0b00100, 0b00100, 0b01110]), + ("2", [0b01110, 0b10001, 0b00010, 0b00100, 0b11111]), + ("3", [0b01110, 0b10001, 0b00110, 0b10001, 0b01110]), + ("4", [0b00010, 0b00110, 0b01010, 0b11111, 0b00010]), + ("5", [0b11111, 0b10000, 0b11110, 0b10001, 0b01110]), + ("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]), + ("+", [0b00000, 0b00100, 0b01110, 0b00100, 0b00000]), + (":", [0b00000, 0b00100, 0b00000, 0b00100, 0b00000]), + ("%", [0b10001, 0b00010, 0b00100, 0b01000, 0b10001]), + ("/", [0b00001, 0b00010, 0b00100, 0b01000, 0b10000]), + (".", [0b00000, 0b00000, 0b00000, 0b00000, 0b00100]), + (",", [0b00000, 0b00000, 0b00000, 0b00100, 0b01000]), + ("(", [0b00010, 0b00100, 0b00100, 0b00100, 0b00010]), + (")", [0b01000, 0b00100, 0b00100, 0b00100, 0b01000]), + ("-", [0b00000, 0b00000, 0b01110, 0b00000, 0b00000]), + ("'", [0b00100, 0b00000, 0b00000, 0b00000, 0b00000]), + // Unicode symbols + ("♥", [0b01010, 0b11111, 0b11111, 0b01110, 0b00100]), + ("♦", [0b00100, 0b01110, 0b11111, 0b01110, 0b00100]), + ("⌂", [0b01110, 0b10001, 0b10101, 0b10001, 0b11111]), + ("[", [0b01110, 0b01000, 0b01000, 0b01000, 0b01110]), + ("]", [0b01110, 0b00010, 0b00010, 0b00010, 0b01110]), + (" ", [0b00000; 5]), +]; + +const LETTER_W: usize = 5; +const LETTER_H: usize = 5; + pub struct Renderer { screen_w: usize, screen_h: usize, - /// Precomputed: floor_dist_lut[row] = perpendicular distance to floor at that row. floor_dist_lut: Vec, - /// Precomputed: floor_bright_lut[row] = brightness × 256 for that row (fixed-point). - floor_bright_lut: Vec, } impl Renderer { pub fn new(screen_w: usize, screen_h: usize) -> Self { let half_h = screen_h as f64 / 2.0; let mut floor_dist_lut = vec![0.0f64; screen_h]; - let mut floor_bright_lut = vec![0u32; screen_h]; - // Only rows below the horizon have meaningful floor values. for py in (screen_h / 2 + 1)..screen_h { let dy = py as f64 - half_h; - let dist = 0.5 * screen_h as f64 / dy; - floor_dist_lut[py] = dist; - let bright = (1.0 / (1.0 + dist * 0.15)).max(0.05); - floor_bright_lut[py] = (bright * 256.0) as u32; + floor_dist_lut[py] = 0.5 * screen_h as f64 / dy; } Self { screen_w, screen_h, floor_dist_lut, - floor_bright_lut, } } - /// Produce one frame into `buffer` (packed `0xAARRGGBB`). pub fn render(&self, game: &Game, buffer: &mut [u32]) { - buffer.fill(0xFF00_0000); // pure-black ceiling + buffer.fill(0xFF00_0000); let half_h = self.screen_h as f64 / 2.0; - let fov = std::f64::consts::PI / 3.0; // 60° + let fov = std::f64::consts::PI / 3.0; + + // Per-column wall distances for entity occlusion. + let mut wall_dists = vec![f64::MAX; self.screen_w]; for sx in 0..self.screen_w { let cam_x = 2.0 * sx as f64 / self.screen_w as f64 - 1.0; @@ -46,15 +106,10 @@ impl Renderer { let rdx = ray_angle.cos(); let rdy = ray_angle.sin(); - // Floor uses perpendicular distance from the LUT, but needs - // Euclidean distance for correct world-position projection. - // Scale by 1/cos(angle_diff) so that rdx_floor / rdy_floor - // map floor_dist → Euclidean travel along the ray. let cos_diff = (ray_angle - game.player.angle).cos().abs().max(0.001); let rdx_floor = rdx / cos_diff; let rdy_floor = rdy / cos_diff; - // ---- DDA setup ------------------------------------------------- let mut map_x = game.player.x as isize; let mut map_y = game.player.y as isize; @@ -84,7 +139,6 @@ impl Renderer { let mut hit_side: bool; let euclidean: f64; - // ---- DDA loop -------------------------------------------------- loop { if side_x < side_y { side_x += ddx; @@ -115,16 +169,14 @@ impl Renderer { } } - // Fisheye correction let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); + wall_dists[sx] = perp_dist; - // ---- wall slice ------------------------------------------------ let wall_h = (self.screen_h as f64 / perp_dist) as isize; let draw_start = ((-wall_h / 2) + self.screen_h as isize / 2).max(0) as usize; let draw_end = ((wall_h / 2) + self.screen_h as isize / 2) .min(self.screen_h as isize - 1) as usize; - // Texture x-coordinate (uses Euclidean, not perp) let wall_x = if !hit_side { game.player.y + euclidean * rdy } else { @@ -133,9 +185,10 @@ impl Renderer { let wall_x = wall_x - wall_x.floor(); let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); - // Hoist: brightness is identical for every pixel in this column let wall_bright_fx = { - let b = (1.0 / (1.0 + perp_dist * 0.12)).max(0.08); + let w = game.weakness_factor(); + let darken_scale = 1.0 + w * 12.0; + let b = 1.0 / (1.0 + perp_dist * 0.25 * darken_scale); (b * 256.0) as u32 }; @@ -143,39 +196,322 @@ impl Renderer { let tex_y = ((py as f64 - half_h + wall_h as f64 / 2.0) * TEX_SIZE as f64 / wall_h as f64) as usize; let tex_y = tex_y.min(TEX_SIZE - 1); - let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); } - // ---- floor below the wall -------------------------------------- let floor_start = draw_end + 1; if floor_start < self.screen_h { + let w = game.weakness_factor(); + let darken_scale = 1.0 + w * 12.0; for py in floor_start..self.screen_h { let floor_dist = self.floor_dist_lut[py]; + let bright = 1.0 / (1.0 + floor_dist * 0.35 * darken_scale); + let bright_fx = (bright * 256.0) as u32; if floor_dist > 15.0 { - // Far floor → just a dark pixel, skip texture & world calc. - let gray = darken_fx(0xFF80_8080, self.floor_bright_lut[py]); + let gray = darken_fx(0xFF80_8080, bright_fx); buffer[py * self.screen_w + sx] = gray; } else { let fx = game.player.x + floor_dist * rdx_floor; let fy = game.player.y + floor_dist * rdy_floor; - let ftx = ((fx.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); let fty = ((fy.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); + let color = + if fx as usize == game.maze.exit_x && fy as usize == game.maze.exit_y { + game.exit_texture[fty * TEX_SIZE + ftx] + } else { + game.floor_texture[fty * TEX_SIZE + ftx] + }; + buffer[py * self.screen_w + sx] = darken_fx(color, bright_fx); + } + } + } + } + + // ---- entity sprites (drawn after walls so occlusion is easy) ---- + self.draw_entities(game, buffer, &wall_dists, fov, half_h); + + // ---- camera bob ---- + let bob = game.camera_bob().round() as isize; + if bob != 0 { + if bob > 0 { + let shift = bob as usize; + for py in (shift..self.screen_h).rev() { + let src = (py - shift) * self.screen_w; + let dst = py * self.screen_w; + buffer.copy_within(src..src + self.screen_w, dst); + } + buffer[0..shift * self.screen_w].fill(0xFF00_0000); + } else { + let shift = (-bob) as usize; + for py in 0..self.screen_h - shift { + let src = (py + shift) * self.screen_w; + let dst = py * self.screen_w; + buffer.copy_within(src..src + self.screen_w, dst); + } + let start = (self.screen_h - shift) * self.screen_w; + buffer[start..].fill(0xFF00_0000); + } + } + + // ---- HUD ---- + self.draw_hud(buffer, game); + } + + // ---- entity sprites --------------------------------------------------- + + fn draw_entities( + &self, + game: &Game, + buffer: &mut [u32], + wall_dists: &[f64], + fov: f64, + half_h: f64, + ) { + let mw = game.maze.width(); + let mh = game.maze.height(); + + for y in 0..mh { + for x in 0..mw { + let entity = match game.maze.entity_at(x, y) { + Some(e) => e, + None => continue, + }; + + let ex = x as f64 + 0.5; + let ey = y as f64 + 0.5; + + let dx = ex - game.player.x; + let dy = ey - game.player.y; + + let fx = game.player.angle.cos(); + let fy = game.player.angle.sin(); + let rx = -fy; + let ry = fx; + + let cam_z = dx * fx + dy * fy; // depth + if cam_z <= 0.1 { + continue; // behind camera + } + + 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 { + continue; + } + + 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 { + continue; + } + + let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); + + self.draw_char_scaled( + buffer, + ch, + sx, + sy, + scale, + color, + cam_z, + game.weakness_factor(), + ); + } + } + } + + fn draw_char_scaled( + &self, + buffer: &mut [u32], + ch: char, + cx: isize, + cy: isize, + scale: f64, + color: u32, + distance: f64, + weakness: f64, + ) { + let glyph = match find_glyph(ch) { + Some(g) => g, + None => return, + }; + + let darken_scale = 1.0 + weakness * 12.0; + let bright = 1.0 / (1.0 + distance * 0.15 * darken_scale); + let bright_fx = (bright * 256.0) as u32; + let final_color = darken_fx(color, bright_fx); + + let draw_w = (LETTER_W as f64 * scale) as isize; + let draw_h = (LETTER_H as f64 * scale) as isize; + + let start_x = cx - draw_w / 2; + let start_y = cy - draw_h / 2; - let color = game.floor_texture[fty * TEX_SIZE + ftx]; - buffer[py * self.screen_w + sx] = - darken_fx(color, self.floor_bright_lut[py]); + for gy in 0..LETTER_H { + let bits = glyph[gy]; + for gx in 0..LETTER_W { + if (bits >> (LETTER_W - 1 - gx)) & 1 == 0 { + continue; + } + let px_start = start_x + (gx as f64 * scale) as isize; + let py_start = start_y + (gy as f64 * scale) as isize; + let px_end = start_x + ((gx + 1) as f64 * scale) as isize; + let py_end = start_y + ((gy + 1) as f64 * scale) as isize; + + 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) { + let idx = py as usize * self.screen_w + px as usize; + buffer[idx] = final_color; + } + } + } + } + } + + // ---- HUD ------------------------------------------------------------- + + fn draw_hud(&self, buffer: &mut [u32], game: &Game) { + let bar_h = 40usize; + let bar_y = self.screen_h - bar_h; + + // Background. + for py in bar_y..self.screen_h { + for px in 0..self.screen_w { + buffer[py * self.screen_w + px] = 0xFF_222222; + } + } + + let x = 8usize; + let y = bar_y + 6; + + // Format the HUD string. + let hp_str = format!("♥{}/{}", game.hp, game.max_hp); + let san_str = format!("♦{}%", game.sanity); + let weak_str = format!("⌂{}%", game.weakness); + let beats_str = format!("♥♥{}", game.heartbeats()); + let wpn_str = format!("WPN:{}", game.weapon_base_damage); + let depth_str = format!("DEPTH:{}", game.depth); + + let inv_str: String = game + .inventory + .iter() + .enumerate() + .map(|(i, item)| match item { + Some(item) => { + let ch = pickup_char(item.pickup); + if i == game.selected_slot { + format!("[{}]", ch) + } else { + format!(" {} ", ch) + } + } + None => { + if i == game.selected_slot { + "[ ]".to_string() + } else { + " ".to_string() + } + } + }) + .collect(); + + let full = format!( + "{} {} {} {} {} {} {}", + hp_str, san_str, weak_str, beats_str, wpn_str, inv_str, depth_str + ); + + self.draw_text(buffer, x, y, &full, 0xFF_CCCCCC); + + // ---- message log above the bar ---- + if !game.message.is_empty() { + let msg_y = bar_y.saturating_sub(LETTER_H + 4); + self.draw_text(buffer, 8, msg_y, &game.message, 0xFF_DDAA44); + } + } + + fn draw_text(&self, buffer: &mut [u32], mut x: usize, y: usize, text: &str, color: u32) { + for ch in text.to_uppercase().chars() { + let glyph = match find_glyph(ch) { + Some(g) => g, + None => { + x += LETTER_W + 1; + continue; + } + }; + + for gy in 0..LETTER_H { + let bits = glyph[gy]; + for gx in 0..LETTER_W { + if (bits >> (LETTER_W - 1 - gx)) & 1 != 0 { + let px = x + gx; + let py = y + gy; + if px < self.screen_w && py < self.screen_h { + buffer[py * self.screen_w + px] = color; + } } } } + x += LETTER_W + 1; // 1px gap + } + } +} + +// ---- helpers ---- + +fn find_glyph(ch: char) -> Option<[u8; 5]> { + let s = ch.to_string(); + for (name, glyph) in LETTERS { + if *name == s { + return Some(*glyph); } } + None +} + +fn entity_char_and_color(entity: &Entity) -> (char, u32) { + match entity { + Entity::Enemy { .. } => ('@', 0xFF_CC3333), + Entity::Hallucination { .. } => ('@', 0xFF_CC3333), + Entity::Pickup(pt) => (pickup_char(*pt), pickup_color(*pt)), + Entity::HallucinatedPickup(pt) => (pickup_char(*pt), pickup_color(*pt)), + } +} + +fn pickup_char(pt: PickupType) -> char { + match pt { + PickupType::Food => 'F', + PickupType::Pill => '0', + PickupType::Bandage => '+', + PickupType::Weapon { .. } => '!', + } +} + +fn pickup_color(pt: PickupType) -> u32 { + match pt { + PickupType::Food => 0xFF_33CC33, + PickupType::Pill => 0xFF_33CCCC, + PickupType::Bandage => 0xFF_CCCCCC, + PickupType::Weapon { .. } => 0xFF_CCCC33, + } } -/// Integer darken: `bright_fx` is brightness × 256 in fixed-point. #[inline(always)] fn darken_fx(color: u32, bright_fx: u32) -> u32 { let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; -- cgit v1.2.3