diff options
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 162 |
1 files changed, 138 insertions, 24 deletions
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])] = &[ | |||
| 38 | ("2", [0b01110, 0b10001, 0b00010, 0b00100, 0b11111]), | 38 | ("2", [0b01110, 0b10001, 0b00010, 0b00100, 0b11111]), |
| 39 | ("3", [0b01110, 0b10001, 0b00110, 0b10001, 0b01110]), | 39 | ("3", [0b01110, 0b10001, 0b00110, 0b10001, 0b01110]), |
| 40 | ("4", [0b00010, 0b00110, 0b01010, 0b11111, 0b00010]), | 40 | ("4", [0b00010, 0b00110, 0b01010, 0b11111, 0b00010]), |
| 41 | ("5", [0b11111, 0b10000, 0b11110, 0b10001, 0b01110]), | 41 | ("5", [0b11111, 0b10000, 0b11110, 0b00001, 0b11110]), |
| 42 | ("6", [0b01110, 0b10000, 0b11110, 0b10001, 0b01110]), | 42 | ("6", [0b01110, 0b10000, 0b11110, 0b10001, 0b01110]), |
| 43 | ("7", [0b11111, 0b00001, 0b00010, 0b00100, 0b00100]), | 43 | ("7", [0b11111, 0b00001, 0b00010, 0b00100, 0b00100]), |
| 44 | ("8", [0b01110, 0b10001, 0b01110, 0b10001, 0b01110]), | 44 | ("8", [0b01110, 0b10001, 0b01110, 0b10001, 0b01110]), |
| 45 | ("9", [0b01110, 0b10001, 0b01111, 0b00001, 0b01110]), | 45 | ("9", [0b01110, 0b10001, 0b01111, 0b00001, 0b01110]), |
| 46 | // Punctuation & symbols | 46 | // Punctuation & symbols |
| 47 | ("!", [0b00100, 0b00100, 0b00100, 0b00000, 0b00100]), | 47 | ("!", [0b00100, 0b00100, 0b00100, 0b00000, 0b00100]), |
| 48 | ("@", [0b01110, 0b10001, 0b10111, 0b10000, 0b01110]), | 48 | ("@", [0b01110, 0b10001, 0b10110, 0b10000, 0b01110]), |
| 49 | ("+", [0b00000, 0b00100, 0b01110, 0b00100, 0b00000]), | 49 | ("+", [0b00000, 0b00100, 0b01110, 0b00100, 0b00000]), |
| 50 | (":", [0b00000, 0b00100, 0b00000, 0b00100, 0b00000]), | 50 | (":", [0b00000, 0b00100, 0b00000, 0b00100, 0b00000]), |
| 51 | ("%", [0b10001, 0b00010, 0b00100, 0b01000, 0b10001]), | 51 | ("%", [0b10001, 0b00010, 0b00100, 0b01000, 0b10001]), |
| @@ -98,6 +98,8 @@ impl Renderer { | |||
| 98 | let fov = std::f64::consts::PI / 3.0; | 98 | let fov = std::f64::consts::PI / 3.0; |
| 99 | 99 | ||
| 100 | // Per-column wall distances for entity occlusion. | 100 | // Per-column wall distances for entity occlusion. |
| 101 | // Store Euclidean (for occlusion) and perp (for rendering). | ||
| 102 | let mut wall_euclidean = vec![f64::MAX; self.screen_w]; | ||
| 101 | let mut wall_dists = vec![f64::MAX; self.screen_w]; | 103 | let mut wall_dists = vec![f64::MAX; self.screen_w]; |
| 102 | 104 | ||
| 103 | for sx in 0..self.screen_w { | 105 | for sx in 0..self.screen_w { |
| @@ -170,6 +172,7 @@ impl Renderer { | |||
| 170 | } | 172 | } |
| 171 | 173 | ||
| 172 | let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); | 174 | let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); |
| 175 | wall_euclidean[sx] = euclidean; | ||
| 173 | wall_dists[sx] = perp_dist; | 176 | wall_dists[sx] = perp_dist; |
| 174 | 177 | ||
| 175 | let wall_h = (self.screen_h as f64 / perp_dist) as isize; | 178 | let wall_h = (self.screen_h as f64 / perp_dist) as isize; |
| @@ -230,7 +233,7 @@ impl Renderer { | |||
| 230 | } | 233 | } |
| 231 | 234 | ||
| 232 | // ---- entity sprites (drawn after walls so occlusion is easy) ---- | 235 | // ---- entity sprites (drawn after walls so occlusion is easy) ---- |
| 233 | self.draw_entities(game, buffer, &wall_dists, fov, half_h); | 236 | self.draw_entities(game, buffer, &wall_euclidean, fov, half_h); |
| 234 | 237 | ||
| 235 | // ---- camera bob ---- | 238 | // ---- camera bob ---- |
| 236 | let bob = game.camera_bob().round() as isize; | 239 | let bob = game.camera_bob().round() as isize; |
| @@ -257,6 +260,33 @@ impl Renderer { | |||
| 257 | 260 | ||
| 258 | // ---- HUD ---- | 261 | // ---- HUD ---- |
| 259 | self.draw_hud(buffer, game); | 262 | self.draw_hud(buffer, game); |
| 263 | |||
| 264 | // ---- controls overlay (shown on start) ---- | ||
| 265 | if game.show_controls { | ||
| 266 | self.draw_controls_overlay(buffer); | ||
| 267 | } | ||
| 268 | |||
| 269 | // ---- screen flash ---- | ||
| 270 | if game.flash_alpha > 0.001 { | ||
| 271 | let r = ((game.flash_color >> 16) & 0xFF) as f64; | ||
| 272 | let g = ((game.flash_color >> 8) & 0xFF) as f64; | ||
| 273 | let b = (game.flash_color & 0xFF) as f64; | ||
| 274 | let a = game.flash_alpha; | ||
| 275 | for py in 0..self.screen_h { | ||
| 276 | for px in 0..self.screen_w { | ||
| 277 | let idx = py * self.screen_w + px; | ||
| 278 | let existing = buffer[idx]; | ||
| 279 | let er = ((existing >> 16) & 0xFF) as f64; | ||
| 280 | let eg = ((existing >> 8) & 0xFF) as f64; | ||
| 281 | let eb = (existing & 0xFF) as f64; | ||
| 282 | let fr = (er * (1.0 - a) + r * a) as u8; | ||
| 283 | let fg = (eg * (1.0 - a) + g * a) as u8; | ||
| 284 | let fb = (eb * (1.0 - a) + b * a) as u8; | ||
| 285 | buffer[idx] = | ||
| 286 | 0xFF00_0000 | ((fr as u32) << 16) | ((fg as u32) << 8) | (fb as u32); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | ||
| 260 | } | 290 | } |
| 261 | 291 | ||
| 262 | // ---- entity sprites --------------------------------------------------- | 292 | // ---- entity sprites --------------------------------------------------- |
| @@ -265,13 +295,25 @@ impl Renderer { | |||
| 265 | &self, | 295 | &self, |
| 266 | game: &Game, | 296 | game: &Game, |
| 267 | buffer: &mut [u32], | 297 | buffer: &mut [u32], |
| 268 | wall_dists: &[f64], | 298 | wall_euclidean: &[f64], |
| 269 | fov: f64, | 299 | fov: f64, |
| 270 | half_h: f64, | 300 | half_h: f64, |
| 271 | ) { | 301 | ) { |
| 272 | let mw = game.maze.width(); | 302 | let mw = game.maze.width(); |
| 273 | let mh = game.maze.height(); | 303 | let mh = game.maze.height(); |
| 274 | 304 | ||
| 305 | // Collect visible entities with their screen info. | ||
| 306 | struct Sprite { | ||
| 307 | sx: isize, | ||
| 308 | sy: isize, | ||
| 309 | scale: f64, | ||
| 310 | ch: char, | ||
| 311 | color: u32, | ||
| 312 | cam_z: f64, | ||
| 313 | } | ||
| 314 | |||
| 315 | let mut sprites: Vec<Sprite> = Vec::new(); | ||
| 316 | |||
| 275 | for y in 0..mh { | 317 | for y in 0..mh { |
| 276 | for x in 0..mw { | 318 | for x in 0..mw { |
| 277 | let entity = match game.maze.entity_at(x, y) { | 319 | let entity = match game.maze.entity_at(x, y) { |
| @@ -290,14 +332,13 @@ impl Renderer { | |||
| 290 | let rx = -fy; | 332 | let rx = -fy; |
| 291 | let ry = fx; | 333 | let ry = fx; |
| 292 | 334 | ||
| 293 | let cam_z = dx * fx + dy * fy; // depth | 335 | let cam_z = dx * fx + dy * fy; |
| 294 | if cam_z <= 0.1 { | 336 | if cam_z <= 0.1 { |
| 295 | continue; // behind camera | 337 | continue; |
| 296 | } | 338 | } |
| 297 | 339 | ||
| 298 | let cam_x = dx * rx + dy * ry; | 340 | let cam_x = dx * rx + dy * ry; |
| 299 | 341 | ||
| 300 | // Check if entity is within FOV. | ||
| 301 | let half_fov_tan = (fov / 2.0).tan(); | 342 | let half_fov_tan = (fov / 2.0).tan(); |
| 302 | let screen_x_norm = cam_x / (cam_z * half_fov_tan); | 343 | let screen_x_norm = cam_x / (cam_z * half_fov_tan); |
| 303 | if screen_x_norm < -1.0 || screen_x_norm > 1.0 { | 344 | if screen_x_norm < -1.0 || screen_x_norm > 1.0 { |
| @@ -306,35 +347,46 @@ impl Renderer { | |||
| 306 | 347 | ||
| 307 | let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize; | 348 | let sx = ((screen_x_norm + 1.0) * 0.5 * self.screen_w as f64) as isize; |
| 308 | 349 | ||
| 309 | // Entity height on screen. | ||
| 310 | let entity_h = (self.screen_h as f64 / cam_z * 0.4) as isize; | 350 | 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; | 351 | let sy = (half_h - entity_h as f64 * 0.3) as isize; |
| 312 | 352 | ||
| 313 | let (ch, color) = entity_char_and_color(entity); | 353 | // Occlusion: entity is visible only if its centre column is not behind a wall. |
| 314 | 354 | // Compare Euclidean distance to entity vs Euclidean distance to wall. | |
| 315 | // Check occlusion: is any wall column this entity overlaps with closer? | 355 | let col = sx.clamp(0, self.screen_w as isize - 1) as usize; |
| 316 | let left = (sx - entity_h / 2).max(0) as usize; | 356 | if cam_z >= wall_euclidean[col] { |
| 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; | 357 | continue; |
| 322 | } | 358 | } |
| 323 | 359 | ||
| 324 | let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); | 360 | let scale = (entity_h.max(6) as f64 / LETTER_H as f64).max(0.5); |
| 361 | let (ch, color) = entity_char_and_color(entity); | ||
| 325 | 362 | ||
| 326 | self.draw_char_scaled( | 363 | sprites.push(Sprite { |
| 327 | buffer, | ||
| 328 | ch, | ||
| 329 | sx, | 364 | sx, |
| 330 | sy, | 365 | sy, |
| 331 | scale, | 366 | scale, |
| 367 | ch, | ||
| 332 | color, | 368 | color, |
| 333 | cam_z, | 369 | cam_z, |
| 334 | game.weakness_factor(), | 370 | }); |
| 335 | ); | ||
| 336 | } | 371 | } |
| 337 | } | 372 | } |
| 373 | |||
| 374 | // Sort farthest first so nearby entities draw on top. | ||
| 375 | sprites.sort_by(|a, b| b.cam_z.partial_cmp(&a.cam_z).unwrap()); | ||
| 376 | |||
| 377 | for s in &sprites { | ||
| 378 | self.draw_char_scaled( | ||
| 379 | buffer, | ||
| 380 | s.ch, | ||
| 381 | s.sx, | ||
| 382 | s.sy, | ||
| 383 | s.scale, | ||
| 384 | s.color, | ||
| 385 | s.cam_z, | ||
| 386 | game.weakness_factor(), | ||
| 387 | wall_euclidean, | ||
| 388 | ); | ||
| 389 | } | ||
| 338 | } | 390 | } |
| 339 | 391 | ||
| 340 | fn draw_char_scaled( | 392 | fn draw_char_scaled( |
| @@ -345,8 +397,9 @@ impl Renderer { | |||
| 345 | cy: isize, | 397 | cy: isize, |
| 346 | scale: f64, | 398 | scale: f64, |
| 347 | color: u32, | 399 | color: u32, |
| 348 | distance: f64, | 400 | cam_z: f64, |
| 349 | weakness: f64, | 401 | weakness: f64, |
| 402 | wall_euclidean: &[f64], | ||
| 350 | ) { | 403 | ) { |
| 351 | let glyph = match find_glyph(ch) { | 404 | let glyph = match find_glyph(ch) { |
| 352 | Some(g) => g, | 405 | Some(g) => g, |
| @@ -354,7 +407,7 @@ impl Renderer { | |||
| 354 | }; | 407 | }; |
| 355 | 408 | ||
| 356 | let darken_scale = 1.0 + weakness * 12.0; | 409 | let darken_scale = 1.0 + weakness * 12.0; |
| 357 | let bright = 1.0 / (1.0 + distance * 0.15 * darken_scale); | 410 | let bright = 1.0 / (1.0 + cam_z * 0.15 * darken_scale); |
| 358 | let bright_fx = (bright * 256.0) as u32; | 411 | let bright_fx = (bright * 256.0) as u32; |
| 359 | let final_color = darken_fx(color, bright_fx); | 412 | let final_color = darken_fx(color, bright_fx); |
| 360 | 413 | ||
| @@ -377,6 +430,10 @@ impl Renderer { | |||
| 377 | 430 | ||
| 378 | for py in py_start.max(0)..py_end.min(self.screen_h as isize) { | 431 | 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) { | 432 | for px in px_start.max(0)..px_end.min(self.screen_w as isize) { |
| 433 | // Per-column occlusion: skip if wall is closer. | ||
| 434 | if cam_z >= wall_euclidean[px as usize] { | ||
| 435 | continue; | ||
| 436 | } | ||
| 380 | let idx = py as usize * self.screen_w + px as usize; | 437 | let idx = py as usize * self.screen_w + px as usize; |
| 381 | buffer[idx] = final_color; | 438 | buffer[idx] = final_color; |
| 382 | } | 439 | } |
| @@ -468,9 +525,66 @@ impl Renderer { | |||
| 468 | } | 525 | } |
| 469 | } | 526 | } |
| 470 | } | 527 | } |
| 471 | x += LETTER_W + 1; // 1px gap | 528 | x += LETTER_W + 1; |
| 472 | } | 529 | } |
| 473 | } | 530 | } |
| 531 | |||
| 532 | fn draw_controls_overlay(&self, buffer: &mut [u32]) { | ||
| 533 | // Semi-transparent darken. | ||
| 534 | for py in 0..self.screen_h { | ||
| 535 | for px in 0..self.screen_w { | ||
| 536 | let idx = py * self.screen_w + px; | ||
| 537 | let c = buffer[idx]; | ||
| 538 | let r = ((c >> 16) & 0xFF) as u32; | ||
| 539 | let g = ((c >> 8) & 0xFF) as u32; | ||
| 540 | let b = (c & 0xFF) as u32; | ||
| 541 | buffer[idx] = 0xFF00_0000 | ((r / 3) << 16) | ((g / 3) << 8) | (b / 3); | ||
| 542 | } | ||
| 543 | } | ||
| 544 | |||
| 545 | let title_color = 0xFF_CC8844; | ||
| 546 | let key_color = 0xFF_88CC88; | ||
| 547 | let desc_color = 0xFF_CCCCCC; | ||
| 548 | let hint_color = 0xFF_888888; | ||
| 549 | |||
| 550 | let cx = self.screen_w / 2; | ||
| 551 | let mut y = 40usize; | ||
| 552 | |||
| 553 | // Title. | ||
| 554 | let title = "DUNGEON CRAWLER"; | ||
| 555 | let title_w = title.len() * (LETTER_W + 1) - 1; | ||
| 556 | self.draw_text(buffer, cx - title_w / 2, y, title, title_color); | ||
| 557 | y += LETTER_H + 10; | ||
| 558 | |||
| 559 | // Controls. | ||
| 560 | let lines: &[(&str, &str)] = &[ | ||
| 561 | ("WASD", "MOVE / STRAFE"), | ||
| 562 | ("Q / E", "TURN 90 LEFT / RIGHT"), | ||
| 563 | ("SPACE", "ATTACK"), | ||
| 564 | ("Z", "USE SELECTED ITEM"), | ||
| 565 | ("X", "DISCARD SELECTED ITEM"), | ||
| 566 | ("1 - 4", "SELECT INVENTORY SLOT"), | ||
| 567 | ("C", "SKIP TURN"), | ||
| 568 | ("ESC", "QUIT"), | ||
| 569 | ]; | ||
| 570 | |||
| 571 | for (key, desc) in lines { | ||
| 572 | let key_px = key.len() * (LETTER_W + 1); | ||
| 573 | let gap_px = 2 * (LETTER_W + 1); | ||
| 574 | let desc_px = desc.len() * (LETTER_W + 1); | ||
| 575 | let total_w = key_px + gap_px + desc_px; | ||
| 576 | let start_x = cx - total_w / 2; | ||
| 577 | |||
| 578 | self.draw_text(buffer, start_x, y, key, key_color); | ||
| 579 | self.draw_text(buffer, start_x + key_px + gap_px, y, desc, desc_color); | ||
| 580 | y += LETTER_H + 4; | ||
| 581 | } | ||
| 582 | |||
| 583 | y += 12; | ||
| 584 | let hint = "PRESS ANY KEY TO BEGIN"; | ||
| 585 | let hint_w = hint.len() * (LETTER_W + 1) - 1; | ||
| 586 | self.draw_text(buffer, cx - hint_w / 2, y, hint, hint_color); | ||
| 587 | } | ||
| 474 | } | 588 | } |
| 475 | 589 | ||
| 476 | // ---- helpers ---- | 590 | // ---- helpers ---- |
