diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-10 21:16:59 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-10 21:16:59 +0200 |
| commit | c8eae871002b57f45b29a2d158e384433e76a90c (patch) | |
| tree | f521d4535649ee4d515271af58b842a8e192e20f /src/renderer.rs | |
| parent | fix floor warping (diff) | |
| download | dungeon-c8eae871002b57f45b29a2d158e384433e76a90c.tar.gz dungeon-c8eae871002b57f45b29a2d158e384433e76a90c.zip | |
Phase 2
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 404 |
1 files changed, 370 insertions, 34 deletions
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 @@ | |||
| 1 | use crate::game::Game; | 1 | use crate::game::{Entity, Game, PickupType}; |
| 2 | use crate::texture::TEX_SIZE; | 2 | use crate::texture::TEX_SIZE; |
| 3 | 3 | ||
| 4 | // ---- simple 5×5 ASCII glyphs ---- | ||
| 5 | // ---- simple 5×5 ASCII glyphs for letters & symbols (incomplete — add as needed) ---- | ||
| 6 | |||
| 7 | const LETTERS: &[(&str, [u8; 5])] = &[ | ||
| 8 | // Uppercase A-Z | ||
| 9 | ("A", [0b01110, 0b10001, 0b11111, 0b10001, 0b10001]), | ||
| 10 | ("B", [0b11110, 0b10001, 0b11110, 0b10001, 0b11110]), | ||
| 11 | ("C", [0b01110, 0b10001, 0b10000, 0b10001, 0b01110]), | ||
| 12 | ("D", [0b11110, 0b10001, 0b10001, 0b10001, 0b11110]), | ||
| 13 | ("E", [0b11111, 0b10000, 0b11110, 0b10000, 0b11111]), | ||
| 14 | ("F", [0b11111, 0b10000, 0b11110, 0b10000, 0b10000]), | ||
| 15 | ("G", [0b01110, 0b10000, 0b10111, 0b10001, 0b01110]), | ||
| 16 | ("H", [0b10001, 0b10001, 0b11111, 0b10001, 0b10001]), | ||
| 17 | ("I", [0b01110, 0b00100, 0b00100, 0b00100, 0b01110]), | ||
| 18 | ("J", [0b00011, 0b00001, 0b00001, 0b10001, 0b01110]), | ||
| 19 | ("K", [0b10001, 0b10010, 0b11100, 0b10010, 0b10001]), | ||
| 20 | ("L", [0b10000, 0b10000, 0b10000, 0b10000, 0b11111]), | ||
| 21 | ("M", [0b10001, 0b11011, 0b10101, 0b10001, 0b10001]), | ||
| 22 | ("N", [0b10001, 0b11001, 0b10101, 0b10011, 0b10001]), | ||
| 23 | ("O", [0b01110, 0b10001, 0b10001, 0b10001, 0b01110]), | ||
| 24 | ("P", [0b11110, 0b10001, 0b11110, 0b10000, 0b10000]), | ||
| 25 | ("Q", [0b01110, 0b10001, 0b10001, 0b10010, 0b01101]), | ||
| 26 | ("R", [0b11110, 0b10001, 0b11110, 0b10010, 0b10001]), | ||
| 27 | ("S", [0b01110, 0b10000, 0b01110, 0b00001, 0b11110]), | ||
| 28 | ("T", [0b11111, 0b00100, 0b00100, 0b00100, 0b00100]), | ||
| 29 | ("U", [0b10001, 0b10001, 0b10001, 0b10001, 0b01110]), | ||
| 30 | ("V", [0b10001, 0b10001, 0b10001, 0b01010, 0b00100]), | ||
| 31 | ("W", [0b10001, 0b10001, 0b10101, 0b10101, 0b01010]), | ||
| 32 | ("X", [0b10001, 0b01010, 0b00100, 0b01010, 0b10001]), | ||
| 33 | ("Y", [0b10001, 0b01010, 0b00100, 0b00100, 0b00100]), | ||
| 34 | ("Z", [0b11111, 0b00010, 0b00100, 0b01000, 0b11111]), | ||
| 35 | // Digits | ||
| 36 | ("0", [0b01110, 0b10011, 0b10101, 0b11001, 0b01110]), | ||
| 37 | ("1", [0b00100, 0b01100, 0b00100, 0b00100, 0b01110]), | ||
| 38 | ("2", [0b01110, 0b10001, 0b00010, 0b00100, 0b11111]), | ||
| 39 | ("3", [0b01110, 0b10001, 0b00110, 0b10001, 0b01110]), | ||
| 40 | ("4", [0b00010, 0b00110, 0b01010, 0b11111, 0b00010]), | ||
| 41 | ("5", [0b11111, 0b10000, 0b11110, 0b10001, 0b01110]), | ||
| 42 | ("6", [0b01110, 0b10000, 0b11110, 0b10001, 0b01110]), | ||
| 43 | ("7", [0b11111, 0b00001, 0b00010, 0b00100, 0b00100]), | ||
| 44 | ("8", [0b01110, 0b10001, 0b01110, 0b10001, 0b01110]), | ||
| 45 | ("9", [0b01110, 0b10001, 0b01111, 0b00001, 0b01110]), | ||
| 46 | // Punctuation & symbols | ||
| 47 | ("!", [0b00100, 0b00100, 0b00100, 0b00000, 0b00100]), | ||
| 48 | ("@", [0b01110, 0b10001, 0b10111, 0b10000, 0b01110]), | ||
| 49 | ("+", [0b00000, 0b00100, 0b01110, 0b00100, 0b00000]), | ||
| 50 | (":", [0b00000, 0b00100, 0b00000, 0b00100, 0b00000]), | ||
| 51 | ("%", [0b10001, 0b00010, 0b00100, 0b01000, 0b10001]), | ||
| 52 | ("/", [0b00001, 0b00010, 0b00100, 0b01000, 0b10000]), | ||
| 53 | (".", [0b00000, 0b00000, 0b00000, 0b00000, 0b00100]), | ||
| 54 | (",", [0b00000, 0b00000, 0b00000, 0b00100, 0b01000]), | ||
| 55 | ("(", [0b00010, 0b00100, 0b00100, 0b00100, 0b00010]), | ||
| 56 | (")", [0b01000, 0b00100, 0b00100, 0b00100, 0b01000]), | ||
| 57 | ("-", [0b00000, 0b00000, 0b01110, 0b00000, 0b00000]), | ||
| 58 | ("'", [0b00100, 0b00000, 0b00000, 0b00000, 0b00000]), | ||
| 59 | // Unicode symbols | ||
| 60 | ("♥", [0b01010, 0b11111, 0b11111, 0b01110, 0b00100]), | ||
| 61 | ("♦", [0b00100, 0b01110, 0b11111, 0b01110, 0b00100]), | ||
| 62 | ("⌂", [0b01110, 0b10001, 0b10101, 0b10001, 0b11111]), | ||
| 63 | ("[", [0b01110, 0b01000, 0b01000, 0b01000, 0b01110]), | ||
| 64 | ("]", [0b01110, 0b00010, 0b00010, 0b00010, 0b01110]), | ||
| 65 | (" ", [0b00000; 5]), | ||
| 66 | ]; | ||
| 67 | |||
| 68 | const LETTER_W: usize = 5; | ||
| 69 | const LETTER_H: usize = 5; | ||
| 70 | |||
| 4 | pub struct Renderer { | 71 | pub struct Renderer { |
| 5 | screen_w: usize, | 72 | screen_w: usize, |
| 6 | screen_h: usize, | 73 | screen_h: usize, |
| 7 | /// Precomputed: floor_dist_lut[row] = perpendicular distance to floor at that row. | ||
| 8 | floor_dist_lut: Vec<f64>, | 74 | floor_dist_lut: Vec<f64>, |
| 9 | /// Precomputed: floor_bright_lut[row] = brightness × 256 for that row (fixed-point). | ||
| 10 | floor_bright_lut: Vec<u32>, | ||
| 11 | } | 75 | } |
| 12 | 76 | ||
| 13 | impl Renderer { | 77 | impl Renderer { |
| 14 | pub fn new(screen_w: usize, screen_h: usize) -> Self { | 78 | pub fn new(screen_w: usize, screen_h: usize) -> Self { |
| 15 | let half_h = screen_h as f64 / 2.0; | 79 | let half_h = screen_h as f64 / 2.0; |
| 16 | let mut floor_dist_lut = vec![0.0f64; screen_h]; | 80 | let mut floor_dist_lut = vec![0.0f64; screen_h]; |
| 17 | let mut floor_bright_lut = vec![0u32; screen_h]; | ||
| 18 | 81 | ||
| 19 | // Only rows below the horizon have meaningful floor values. | ||
| 20 | for py in (screen_h / 2 + 1)..screen_h { | 82 | for py in (screen_h / 2 + 1)..screen_h { |
| 21 | let dy = py as f64 - half_h; | 83 | let dy = py as f64 - half_h; |
| 22 | let dist = 0.5 * screen_h as f64 / dy; | 84 | floor_dist_lut[py] = 0.5 * screen_h as f64 / dy; |
| 23 | floor_dist_lut[py] = dist; | ||
| 24 | let bright = (1.0 / (1.0 + dist * 0.15)).max(0.05); | ||
| 25 | floor_bright_lut[py] = (bright * 256.0) as u32; | ||
| 26 | } | 85 | } |
| 27 | 86 | ||
| 28 | Self { | 87 | Self { |
| 29 | screen_w, | 88 | screen_w, |
| 30 | screen_h, | 89 | screen_h, |
| 31 | floor_dist_lut, | 90 | floor_dist_lut, |
| 32 | floor_bright_lut, | ||
| 33 | } | 91 | } |
| 34 | } | 92 | } |
| 35 | 93 | ||
| 36 | /// Produce one frame into `buffer` (packed `0xAARRGGBB`). | ||
| 37 | pub fn render(&self, game: &Game, buffer: &mut [u32]) { | 94 | pub fn render(&self, game: &Game, buffer: &mut [u32]) { |
| 38 | buffer.fill(0xFF00_0000); // pure-black ceiling | 95 | buffer.fill(0xFF00_0000); |
| 39 | 96 | ||
| 40 | let half_h = self.screen_h as f64 / 2.0; | 97 | let half_h = self.screen_h as f64 / 2.0; |
| 41 | let fov = std::f64::consts::PI / 3.0; // 60° | 98 | let fov = std::f64::consts::PI / 3.0; |
| 99 | |||
| 100 | // Per-column wall distances for entity occlusion. | ||
| 101 | let mut wall_dists = vec![f64::MAX; self.screen_w]; | ||
| 42 | 102 | ||
| 43 | for sx in 0..self.screen_w { | 103 | for sx in 0..self.screen_w { |
| 44 | let cam_x = 2.0 * sx as f64 / self.screen_w as f64 - 1.0; | 104 | let cam_x = 2.0 * sx as f64 / self.screen_w as f64 - 1.0; |
| @@ -46,15 +106,10 @@ impl Renderer { | |||
| 46 | let rdx = ray_angle.cos(); | 106 | let rdx = ray_angle.cos(); |
| 47 | let rdy = ray_angle.sin(); | 107 | let rdy = ray_angle.sin(); |
| 48 | 108 | ||
| 49 | // Floor uses perpendicular distance from the LUT, but needs | ||
| 50 | // Euclidean distance for correct world-position projection. | ||
| 51 | // Scale by 1/cos(angle_diff) so that rdx_floor / rdy_floor | ||
| 52 | // map floor_dist → Euclidean travel along the ray. | ||
| 53 | let cos_diff = (ray_angle - game.player.angle).cos().abs().max(0.001); | 109 | let cos_diff = (ray_angle - game.player.angle).cos().abs().max(0.001); |
| 54 | let rdx_floor = rdx / cos_diff; | 110 | let rdx_floor = rdx / cos_diff; |
| 55 | let rdy_floor = rdy / cos_diff; | 111 | let rdy_floor = rdy / cos_diff; |
| 56 | 112 | ||
| 57 | // ---- DDA setup ------------------------------------------------- | ||
| 58 | let mut map_x = game.player.x as isize; | 113 | let mut map_x = game.player.x as isize; |
| 59 | let mut map_y = game.player.y as isize; | 114 | let mut map_y = game.player.y as isize; |
| 60 | 115 | ||
| @@ -84,7 +139,6 @@ impl Renderer { | |||
| 84 | let mut hit_side: bool; | 139 | let mut hit_side: bool; |
| 85 | let euclidean: f64; | 140 | let euclidean: f64; |
| 86 | 141 | ||
| 87 | // ---- DDA loop -------------------------------------------------- | ||
| 88 | loop { | 142 | loop { |
| 89 | if side_x < side_y { | 143 | if side_x < side_y { |
| 90 | side_x += ddx; | 144 | side_x += ddx; |
| @@ -115,16 +169,14 @@ impl Renderer { | |||
| 115 | } | 169 | } |
| 116 | } | 170 | } |
| 117 | 171 | ||
| 118 | // Fisheye correction | ||
| 119 | let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); | 172 | let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); |
| 173 | wall_dists[sx] = perp_dist; | ||
| 120 | 174 | ||
| 121 | // ---- wall slice ------------------------------------------------ | ||
| 122 | let wall_h = (self.screen_h as f64 / perp_dist) as isize; | 175 | let wall_h = (self.screen_h as f64 / perp_dist) as isize; |
| 123 | let draw_start = ((-wall_h / 2) + self.screen_h as isize / 2).max(0) as usize; | 176 | let draw_start = ((-wall_h / 2) + self.screen_h as isize / 2).max(0) as usize; |
| 124 | let draw_end = ((wall_h / 2) + self.screen_h as isize / 2) | 177 | let draw_end = ((wall_h / 2) + self.screen_h as isize / 2) |
| 125 | .min(self.screen_h as isize - 1) as usize; | 178 | .min(self.screen_h as isize - 1) as usize; |
| 126 | 179 | ||
| 127 | // Texture x-coordinate (uses Euclidean, not perp) | ||
| 128 | let wall_x = if !hit_side { | 180 | let wall_x = if !hit_side { |
| 129 | game.player.y + euclidean * rdy | 181 | game.player.y + euclidean * rdy |
| 130 | } else { | 182 | } else { |
| @@ -133,9 +185,10 @@ impl Renderer { | |||
| 133 | let wall_x = wall_x - wall_x.floor(); | 185 | let wall_x = wall_x - wall_x.floor(); |
| 134 | let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | 186 | let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); |
| 135 | 187 | ||
| 136 | // Hoist: brightness is identical for every pixel in this column | ||
| 137 | let wall_bright_fx = { | 188 | let wall_bright_fx = { |
| 138 | let b = (1.0 / (1.0 + perp_dist * 0.12)).max(0.08); | 189 | let w = game.weakness_factor(); |
| 190 | let darken_scale = 1.0 + w * 12.0; | ||
| 191 | let b = 1.0 / (1.0 + perp_dist * 0.25 * darken_scale); | ||
| 139 | (b * 256.0) as u32 | 192 | (b * 256.0) as u32 |
| 140 | }; | 193 | }; |
| 141 | 194 | ||
| @@ -143,39 +196,322 @@ impl Renderer { | |||
| 143 | let tex_y = ((py as f64 - half_h + wall_h as f64 / 2.0) * TEX_SIZE as f64 | 196 | let tex_y = ((py as f64 - half_h + wall_h as f64 / 2.0) * TEX_SIZE as f64 |
| 144 | / wall_h as f64) as usize; | 197 | / wall_h as f64) as usize; |
| 145 | let tex_y = tex_y.min(TEX_SIZE - 1); | 198 | let tex_y = tex_y.min(TEX_SIZE - 1); |
| 146 | |||
| 147 | let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; | 199 | let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; |
| 148 | buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); | 200 | buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); |
| 149 | } | 201 | } |
| 150 | 202 | ||
| 151 | // ---- floor below the wall -------------------------------------- | ||
| 152 | let floor_start = draw_end + 1; | 203 | let floor_start = draw_end + 1; |
| 153 | if floor_start < self.screen_h { | 204 | if floor_start < self.screen_h { |
| 205 | let w = game.weakness_factor(); | ||
| 206 | let darken_scale = 1.0 + w * 12.0; | ||
| 154 | for py in floor_start..self.screen_h { | 207 | for py in floor_start..self.screen_h { |
| 155 | let floor_dist = self.floor_dist_lut[py]; | 208 | let floor_dist = self.floor_dist_lut[py]; |
| 209 | let bright = 1.0 / (1.0 + floor_dist * 0.35 * darken_scale); | ||
| 210 | let bright_fx = (bright * 256.0) as u32; | ||
| 156 | 211 | ||
| 157 | if floor_dist > 15.0 { | 212 | if floor_dist > 15.0 { |
| 158 | // Far floor → just a dark pixel, skip texture & world calc. | 213 | let gray = darken_fx(0xFF80_8080, bright_fx); |
| 159 | let gray = darken_fx(0xFF80_8080, self.floor_bright_lut[py]); | ||
| 160 | buffer[py * self.screen_w + sx] = gray; | 214 | buffer[py * self.screen_w + sx] = gray; |
| 161 | } else { | 215 | } else { |
| 162 | let fx = game.player.x + floor_dist * rdx_floor; | 216 | let fx = game.player.x + floor_dist * rdx_floor; |
| 163 | let fy = game.player.y + floor_dist * rdy_floor; | 217 | let fy = game.player.y + floor_dist * rdy_floor; |
| 164 | |||
| 165 | let ftx = ((fx.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | 218 | let ftx = ((fx.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); |
| 166 | let fty = ((fy.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | 219 | let fty = ((fy.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); |
| 220 | let color = | ||
| 221 | if fx as usize == game.maze.exit_x && fy as usize == game.maze.exit_y { | ||
| 222 | game.exit_texture[fty * TEX_SIZE + ftx] | ||
| 223 | } else { | ||
| 224 | game.floor_texture[fty * TEX_SIZE + ftx] | ||
| 225 | }; | ||
| 226 | buffer[py * self.screen_w + sx] = darken_fx(color, bright_fx); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | // ---- entity sprites (drawn after walls so occlusion is easy) ---- | ||
| 233 | self.draw_entities(game, buffer, &wall_dists, fov, half_h); | ||
| 234 | |||
| 235 | // ---- camera bob ---- | ||
| 236 | let bob = game.camera_bob().round() as isize; | ||
| 237 | if bob != 0 { | ||
| 238 | if bob > 0 { | ||
| 239 | let shift = bob as usize; | ||
| 240 | for py in (shift..self.screen_h).rev() { | ||
| 241 | let src = (py - shift) * self.screen_w; | ||
| 242 | let dst = py * self.screen_w; | ||
| 243 | buffer.copy_within(src..src + self.screen_w, dst); | ||
| 244 | } | ||
| 245 | buffer[0..shift * self.screen_w].fill(0xFF00_0000); | ||
| 246 | } else { | ||
| 247 | let shift = (-bob) as usize; | ||
| 248 | for py in 0..self.screen_h - shift { | ||
| 249 | let src = (py + shift) * self.screen_w; | ||
| 250 | let dst = py * self.screen_w; | ||
| 251 | buffer.copy_within(src..src + self.screen_w, dst); | ||
| 252 | } | ||
| 253 | let start = (self.screen_h - shift) * self.screen_w; | ||
| 254 | buffer[start..].fill(0xFF00_0000); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | // ---- HUD ---- | ||
| 259 | self.draw_hud(buffer, game); | ||
| 260 | } | ||
| 261 | |||
| 262 | // ---- entity sprites --------------------------------------------------- | ||
| 263 | |||
| 264 | fn draw_entities( | ||
| 265 | &self, | ||
| 266 | game: &Game, | ||
| 267 | buffer: &mut [u32], | ||
| 268 | wall_dists: &[f64], | ||
| 269 | fov: f64, | ||
| 270 | half_h: f64, | ||
| 271 | ) { | ||
| 272 | let mw = game.maze.width(); | ||
| 273 | let mh = game.maze.height(); | ||
| 274 | |||
| 275 | for y in 0..mh { | ||
| 276 | for x in 0..mw { | ||
| 277 | let entity = match game.maze.entity_at(x, y) { | ||
| 278 | Some(e) => e, | ||
| 279 | None => continue, | ||
| 280 | }; | ||
| 281 | |||
| 282 | let ex = x as f64 + 0.5; | ||
| 283 | let ey = y as f64 + 0.5; | ||
| 284 | |||
| 285 | let dx = ex - game.player.x; | ||
| 286 | let dy = ey - game.player.y; | ||
| 287 | |||
| 288 | let fx = game.player.angle.cos(); | ||
| 289 | let fy = game.player.angle.sin(); | ||
| 290 | let rx = -fy; | ||
| 291 | let ry = fx; | ||
| 292 | |||
| 293 | let cam_z = dx * fx + dy * fy; // depth | ||
| 294 | if cam_z <= 0.1 { | ||
| 295 | continue; // behind camera | ||
| 296 | } | ||
| 297 | |||
| 298 | let cam_x = dx * rx + dy * ry; | ||
| 299 | |||
| 300 | // Check if entity is within FOV. | ||
| 301 | let half_fov_tan = (fov / 2.0).tan(); | ||
| 302 | let screen_x_norm = cam_x / (cam_z * half_fov_tan); | ||
| 303 | if screen_x_norm < -1.0 || screen_x_norm > 1.0 { | ||
| 304 | continue; | ||
| 305 | } | ||
| 306 | |||
| 307 | let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize; | ||
| 308 | |||
| 309 | // Entity height on screen. | ||
| 310 | let entity_h = (self.screen_h as f64 / cam_z * 0.4) as isize; | ||
| 311 | let sy = (half_h - entity_h as f64 * 0.3) as isize; | ||
| 312 | |||
| 313 | let (ch, color) = entity_char_and_color(entity); | ||
| 314 | |||
| 315 | // Check occlusion: is any wall column this entity overlaps with closer? | ||
| 316 | let left = (sx - entity_h / 2).max(0) as usize; | ||
| 317 | let right = (sx + entity_h / 2).min(self.screen_w as isize - 1) as usize; | ||
| 318 | let visible = (left..=right).any(|col| cam_z < wall_dists[col]); | ||
| 319 | |||
| 320 | if !visible { | ||
| 321 | continue; | ||
| 322 | } | ||
| 323 | |||
| 324 | let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); | ||
| 325 | |||
| 326 | self.draw_char_scaled( | ||
| 327 | buffer, | ||
| 328 | ch, | ||
| 329 | sx, | ||
| 330 | sy, | ||
| 331 | scale, | ||
| 332 | color, | ||
| 333 | cam_z, | ||
| 334 | game.weakness_factor(), | ||
| 335 | ); | ||
| 336 | } | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | fn draw_char_scaled( | ||
| 341 | &self, | ||
| 342 | buffer: &mut [u32], | ||
| 343 | ch: char, | ||
| 344 | cx: isize, | ||
| 345 | cy: isize, | ||
| 346 | scale: f64, | ||
| 347 | color: u32, | ||
| 348 | distance: f64, | ||
| 349 | weakness: f64, | ||
| 350 | ) { | ||
| 351 | let glyph = match find_glyph(ch) { | ||
| 352 | Some(g) => g, | ||
| 353 | None => return, | ||
| 354 | }; | ||
| 355 | |||
| 356 | let darken_scale = 1.0 + weakness * 12.0; | ||
| 357 | let bright = 1.0 / (1.0 + distance * 0.15 * darken_scale); | ||
| 358 | let bright_fx = (bright * 256.0) as u32; | ||
| 359 | let final_color = darken_fx(color, bright_fx); | ||
| 360 | |||
| 361 | let draw_w = (LETTER_W as f64 * scale) as isize; | ||
| 362 | let draw_h = (LETTER_H as f64 * scale) as isize; | ||
| 363 | |||
| 364 | let start_x = cx - draw_w / 2; | ||
| 365 | let start_y = cy - draw_h / 2; | ||
| 167 | 366 | ||
| 168 | let color = game.floor_texture[fty * TEX_SIZE + ftx]; | 367 | for gy in 0..LETTER_H { |
| 169 | buffer[py * self.screen_w + sx] = | 368 | let bits = glyph[gy]; |
| 170 | darken_fx(color, self.floor_bright_lut[py]); | 369 | for gx in 0..LETTER_W { |
| 370 | if (bits >> (LETTER_W - 1 - gx)) & 1 == 0 { | ||
| 371 | continue; | ||
| 372 | } | ||
| 373 | let px_start = start_x + (gx as f64 * scale) as isize; | ||
| 374 | let py_start = start_y + (gy as f64 * scale) as isize; | ||
| 375 | let px_end = start_x + ((gx + 1) as f64 * scale) as isize; | ||
| 376 | let py_end = start_y + ((gy + 1) as f64 * scale) as isize; | ||
| 377 | |||
| 378 | for py in py_start.max(0)..py_end.min(self.screen_h as isize) { | ||
| 379 | for px in px_start.max(0)..px_end.min(self.screen_w as isize) { | ||
| 380 | let idx = py as usize * self.screen_w + px as usize; | ||
| 381 | buffer[idx] = final_color; | ||
| 382 | } | ||
| 383 | } | ||
| 384 | } | ||
| 385 | } | ||
| 386 | } | ||
| 387 | |||
| 388 | // ---- HUD ------------------------------------------------------------- | ||
| 389 | |||
| 390 | fn draw_hud(&self, buffer: &mut [u32], game: &Game) { | ||
| 391 | let bar_h = 40usize; | ||
| 392 | let bar_y = self.screen_h - bar_h; | ||
| 393 | |||
| 394 | // Background. | ||
| 395 | for py in bar_y..self.screen_h { | ||
| 396 | for px in 0..self.screen_w { | ||
| 397 | buffer[py * self.screen_w + px] = 0xFF_222222; | ||
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 401 | let x = 8usize; | ||
| 402 | let y = bar_y + 6; | ||
| 403 | |||
| 404 | // Format the HUD string. | ||
| 405 | let hp_str = format!("♥{}/{}", game.hp, game.max_hp); | ||
| 406 | let san_str = format!("♦{}%", game.sanity); | ||
| 407 | let weak_str = format!("⌂{}%", game.weakness); | ||
| 408 | let beats_str = format!("♥♥{}", game.heartbeats()); | ||
| 409 | let wpn_str = format!("WPN:{}", game.weapon_base_damage); | ||
| 410 | let depth_str = format!("DEPTH:{}", game.depth); | ||
| 411 | |||
| 412 | let inv_str: String = game | ||
| 413 | .inventory | ||
| 414 | .iter() | ||
| 415 | .enumerate() | ||
| 416 | .map(|(i, item)| match item { | ||
| 417 | Some(item) => { | ||
| 418 | let ch = pickup_char(item.pickup); | ||
| 419 | if i == game.selected_slot { | ||
| 420 | format!("[{}]", ch) | ||
| 421 | } else { | ||
| 422 | format!(" {} ", ch) | ||
| 423 | } | ||
| 424 | } | ||
| 425 | None => { | ||
| 426 | if i == game.selected_slot { | ||
| 427 | "[ ]".to_string() | ||
| 428 | } else { | ||
| 429 | " ".to_string() | ||
| 430 | } | ||
| 431 | } | ||
| 432 | }) | ||
| 433 | .collect(); | ||
| 434 | |||
| 435 | let full = format!( | ||
| 436 | "{} {} {} {} {} {} {}", | ||
| 437 | hp_str, san_str, weak_str, beats_str, wpn_str, inv_str, depth_str | ||
| 438 | ); | ||
| 439 | |||
| 440 | self.draw_text(buffer, x, y, &full, 0xFF_CCCCCC); | ||
| 441 | |||
| 442 | // ---- message log above the bar ---- | ||
| 443 | if !game.message.is_empty() { | ||
| 444 | let msg_y = bar_y.saturating_sub(LETTER_H + 4); | ||
| 445 | self.draw_text(buffer, 8, msg_y, &game.message, 0xFF_DDAA44); | ||
| 446 | } | ||
| 447 | } | ||
| 448 | |||
| 449 | fn draw_text(&self, buffer: &mut [u32], mut x: usize, y: usize, text: &str, color: u32) { | ||
| 450 | for ch in text.to_uppercase().chars() { | ||
| 451 | let glyph = match find_glyph(ch) { | ||
| 452 | Some(g) => g, | ||
| 453 | None => { | ||
| 454 | x += LETTER_W + 1; | ||
| 455 | continue; | ||
| 456 | } | ||
| 457 | }; | ||
| 458 | |||
| 459 | for gy in 0..LETTER_H { | ||
| 460 | let bits = glyph[gy]; | ||
| 461 | for gx in 0..LETTER_W { | ||
| 462 | if (bits >> (LETTER_W - 1 - gx)) & 1 != 0 { | ||
| 463 | let px = x + gx; | ||
| 464 | let py = y + gy; | ||
| 465 | if px < self.screen_w && py < self.screen_h { | ||
| 466 | buffer[py * self.screen_w + px] = color; | ||
| 467 | } | ||
| 171 | } | 468 | } |
| 172 | } | 469 | } |
| 173 | } | 470 | } |
| 471 | x += LETTER_W + 1; // 1px gap | ||
| 472 | } | ||
| 473 | } | ||
| 474 | } | ||
| 475 | |||
| 476 | // ---- helpers ---- | ||
| 477 | |||
| 478 | fn find_glyph(ch: char) -> Option<[u8; 5]> { | ||
| 479 | let s = ch.to_string(); | ||
| 480 | for (name, glyph) in LETTERS { | ||
| 481 | if *name == s { | ||
| 482 | return Some(*glyph); | ||
| 174 | } | 483 | } |
| 175 | } | 484 | } |
| 485 | None | ||
| 486 | } | ||
| 487 | |||
| 488 | fn entity_char_and_color(entity: &Entity) -> (char, u32) { | ||
| 489 | match entity { | ||
| 490 | Entity::Enemy { .. } => ('@', 0xFF_CC3333), | ||
| 491 | Entity::Hallucination { .. } => ('@', 0xFF_CC3333), | ||
| 492 | Entity::Pickup(pt) => (pickup_char(*pt), pickup_color(*pt)), | ||
| 493 | Entity::HallucinatedPickup(pt) => (pickup_char(*pt), pickup_color(*pt)), | ||
| 494 | } | ||
| 495 | } | ||
| 496 | |||
| 497 | fn pickup_char(pt: PickupType) -> char { | ||
| 498 | match pt { | ||
| 499 | PickupType::Food => 'F', | ||
| 500 | PickupType::Pill => '0', | ||
| 501 | PickupType::Bandage => '+', | ||
| 502 | PickupType::Weapon { .. } => '!', | ||
| 503 | } | ||
| 504 | } | ||
| 505 | |||
| 506 | fn pickup_color(pt: PickupType) -> u32 { | ||
| 507 | match pt { | ||
| 508 | PickupType::Food => 0xFF_33CC33, | ||
| 509 | PickupType::Pill => 0xFF_33CCCC, | ||
| 510 | PickupType::Bandage => 0xFF_CCCCCC, | ||
| 511 | PickupType::Weapon { .. } => 0xFF_CCCC33, | ||
| 512 | } | ||
| 176 | } | 513 | } |
| 177 | 514 | ||
| 178 | /// Integer darken: `bright_fx` is brightness × 256 in fixed-point. | ||
| 179 | #[inline(always)] | 515 | #[inline(always)] |
| 180 | fn darken_fx(color: u32, bright_fx: u32) -> u32 { | 516 | fn darken_fx(color: u32, bright_fx: u32) -> u32 { |
| 181 | let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; | 517 | let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; |
