use crate::game::{Entity, Game, GameState, 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, 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, 0b10110, 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. // 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 { 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_euclidean[sx] = euclidean; 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_euclidean, 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); // ---- 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); } } } // ---- death screen ---- if game.state == GameState::GameOver { self.draw_death_screen(buffer, game); } } // ---- entity sprites --------------------------------------------------- fn draw_entities( &self, game: &Game, buffer: &mut [u32], 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) { 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; if cam_z <= 0.1 { continue; } let cam_x = dx * rx + dy * ry; 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; 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 scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); let (ch, color) = entity_char_and_color(entity); sprites.push(Sprite { sx, sy, scale, ch, color, cam_z, }); } } // 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( &self, buffer: &mut [u32], ch: char, cx: isize, cy: isize, scale: f64, color: u32, cam_z: f64, weakness: f64, wall_euclidean: &[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 + cam_z * 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) { // 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; } } } } } // ---- 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; } } 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); } fn draw_death_screen(&self, buffer: &mut [u32], game: &Game) { // Full 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 / 5) << 16) | ((g / 5) << 8) | (b / 5); } } let cx = self.screen_w / 2; let mut y = self.screen_h as usize / 2 - 40; let title_color = 0xFF_CC3333; let text_color = 0xFF_CCCCCC; let score_color = 0xFF_CC8844; let hint_color = 0xFF_666666; let title = "YOU HAVE DIED"; let title_w = title.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - title_w / 2, y, title, title_color); y += LETTER_H + 12; let msg = "YOU HAD BECOME YET ANOTHER"; let msg_w = msg.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - msg_w / 2, y, msg, text_color); y += LETTER_H + 4; let msg2 = "LEGEND OF THIS DUNGEON."; let msg2_w = msg2.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - msg2_w / 2, y, msg2, text_color); y += LETTER_H + 16; let score = game.score(); let score_str = format!("SCORE: {}", score); let score_w = score_str.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - score_w / 2, y, &score_str, score_color); y += LETTER_H + 4; let kills_str = format!( "KILLS: {} DEPTH: {} WPN:{}", game.kills, game.depth, game.weapon_base_damage ); let kills_w = kills_str.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - kills_w / 2, y, &kills_str, text_color); y += LETTER_H + 20; let hint = "PRESS ANY KEY TO EXIT"; let hint_w = hint.len() * (LETTER_W + 1) - 1; self.draw_text(buffer, cx - hint_w / 2, y, hint, hint_color); } } // ---- 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 }