summaryrefslogtreecommitdiffstats
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs162
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 ----