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, floor_dist_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]; for py in (screen_h / 2 + 1)..screen_h { let dy = py as f64 - half_h; floor_dist_lut[py] = 0.5 * screen_h as f64 / dy; } Self { screen_w, screen_h, floor_dist_lut, } } pub fn render(&self, game: &Game, buffer: &mut [u32]) { buffer.fill(0xFF00_0000); let half_h = self.screen_h as f64 / 2.0; 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; let ray_angle = game.player.angle + (fov / 2.0) * cam_x; let rdx = ray_angle.cos(); let rdy = ray_angle.sin(); 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; let mut map_x = game.player.x as isize; let mut map_y = game.player.y as isize; let ddx = if rdx == 0.0 { f64::MAX } else { (1.0 / rdx).abs() }; let ddy = if rdy == 0.0 { f64::MAX } else { (1.0 / rdy).abs() }; let (step_x, mut side_x) = if rdx < 0.0 { (-1isize, (game.player.x - map_x as f64) * ddx) } else { (1isize, (map_x as f64 + 1.0 - game.player.x) * ddx) }; let (step_y, mut side_y) = if rdy < 0.0 { (-1isize, (game.player.y - map_y as f64) * ddy) } else { (1isize, (map_y as f64 + 1.0 - game.player.y) * ddy) }; let mut hit_side: bool; let euclidean: f64; loop { if side_x < side_y { side_x += ddx; map_x += step_x; hit_side = false; } else { side_y += ddy; map_y += step_y; hit_side = true; } if map_y < 0 || map_y >= game.maze.height() as isize || map_x < 0 || map_x >= game.maze.width() as isize { euclidean = 1.0; break; } if game.maze.is_wall(map_y as usize, map_x as usize) { euclidean = if !hit_side { (map_x as f64 - game.player.x + (1.0 - step_x as f64) / 2.0) / rdx } else { (map_y as f64 - game.player.y + (1.0 - step_y as f64) / 2.0) / rdy }; break; } } let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); wall_dists[sx] = perp_dist; 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; let wall_x = if !hit_side { game.player.y + euclidean * rdy } else { game.player.x + euclidean * rdx }; let wall_x = wall_x - wall_x.floor(); let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); let wall_bright_fx = { 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 }; for py in draw_start..=draw_end { 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); } 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 { 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; 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, } } #[inline(always)] fn darken_fx(color: u32, bright_fx: u32) -> u32 { let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; let g = (((color >> 8) & 0xFF) * bright_fx) >> 8; let b = ((color & 0xFF) * bright_fx) >> 8; 0xFF00_0000 | (r << 16) | (g << 8) | b }