diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-11 16:05:15 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-11 16:05:15 +0200 |
| commit | a4626cb681fdb4939772018def2019c1d1c49d46 (patch) | |
| tree | 455178e212f225e43aae702b6de23b6be900273c /src/renderer.rs | |
| parent | add floor transition: Space on exit descends to next depth (diff) | |
| download | dungeon-a4626cb681fdb4939772018def2019c1d1c49d46.tar.gz dungeon-a4626cb681fdb4939772018def2019c1d1c49d46.zip | |
add psychedelic horror effects: phantom walls, shifting maze, darkness, color drift, persistent hallucinations
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 71 |
1 files changed, 60 insertions, 11 deletions
diff --git a/src/renderer.rs b/src/renderer.rs index 885e5de..86862f0 100644 --- a/src/renderer.rs +++ b/src/renderer.rs | |||
| @@ -108,6 +108,8 @@ impl Renderer { | |||
| 108 | 108 | ||
| 109 | let half_h = self.screen_h as f64 / 2.0; | 109 | let half_h = self.screen_h as f64 / 2.0; |
| 110 | let fov = std::f64::consts::PI / 3.0; | 110 | let fov = std::f64::consts::PI / 3.0; |
| 111 | let drift = game.color_drift(); | ||
| 112 | let breathing = game.wall_breathing(); | ||
| 111 | 113 | ||
| 112 | // Per-column wall distances for entity occlusion. | 114 | // Per-column wall distances for entity occlusion. |
| 113 | // Store Euclidean (for occlusion) and perp (for rendering). | 115 | // Store Euclidean (for occlusion) and perp (for rendering). |
| @@ -173,7 +175,18 @@ impl Renderer { | |||
| 173 | break; | 175 | break; |
| 174 | } | 176 | } |
| 175 | 177 | ||
| 176 | if game.maze.is_wall(map_y as usize, map_x as usize) { | 178 | let ux = map_x as usize; |
| 179 | let uy = map_y as usize; | ||
| 180 | |||
| 181 | // Phantom passage: a wall that appears as a passage. | ||
| 182 | // Skip it — the ray passes through. | ||
| 183 | if game.maze.is_wall(uy, ux) && game.is_phantom_passage(ux, uy) { | ||
| 184 | continue; | ||
| 185 | } | ||
| 186 | |||
| 187 | // Phantom wall: a passage that appears as a wall. | ||
| 188 | // OR a real wall. Either way, stop the ray here. | ||
| 189 | if game.maze.is_wall(uy, ux) || game.is_phantom_wall(ux, uy) { | ||
| 177 | euclidean = if !hit_side { | 190 | euclidean = if !hit_side { |
| 178 | (map_x as f64 - game.player.x + (1.0 - step_x as f64) / 2.0) / rdx | 191 | (map_x as f64 - game.player.x + (1.0 - step_x as f64) / 2.0) / rdx |
| 179 | } else { | 192 | } else { |
| @@ -198,11 +211,13 @@ impl Renderer { | |||
| 198 | game.player.x + euclidean * rdx | 211 | game.player.x + euclidean * rdx |
| 199 | }; | 212 | }; |
| 200 | let wall_x = wall_x - wall_x.floor(); | 213 | let wall_x = wall_x - wall_x.floor(); |
| 201 | let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | 214 | // Wall breathing: subtle horizontal texture wobble. |
| 215 | let tex_x = (((wall_x + breathing / TEX_SIZE as f64) * TEX_SIZE as f64) as isize) | ||
| 216 | .rem_euclid(TEX_SIZE as isize) as usize; | ||
| 202 | 217 | ||
| 203 | let wall_bright_fx = { | 218 | let wall_bright_fx = { |
| 204 | let w = game.weakness_factor(); | 219 | let d = game.darkness_factor(); |
| 205 | let darken_scale = 1.0 + w * 12.0; | 220 | let darken_scale = 1.0 + d * 12.0; |
| 206 | let b = 1.0 / (1.0 + perp_dist * 0.25 * darken_scale); | 221 | let b = 1.0 / (1.0 + perp_dist * 0.25 * darken_scale); |
| 207 | (b * 256.0) as u32 | 222 | (b * 256.0) as u32 |
| 208 | }; | 223 | }; |
| @@ -212,13 +227,14 @@ impl Renderer { | |||
| 212 | / wall_h as f64) as usize; | 227 | / wall_h as f64) as usize; |
| 213 | let tex_y = tex_y.min(TEX_SIZE - 1); | 228 | let tex_y = tex_y.min(TEX_SIZE - 1); |
| 214 | let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; | 229 | let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; |
| 215 | buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); | 230 | buffer[py * self.screen_w + sx] = |
| 231 | color_drift_fx(darken_fx(color, wall_bright_fx), drift); | ||
| 216 | } | 232 | } |
| 217 | 233 | ||
| 218 | let floor_start = draw_end + 1; | 234 | let floor_start = draw_end + 1; |
| 219 | if floor_start < self.screen_h { | 235 | if floor_start < self.screen_h { |
| 220 | let w = game.weakness_factor(); | 236 | let d = game.darkness_factor(); |
| 221 | let darken_scale = 1.0 + w * 12.0; | 237 | let darken_scale = 1.0 + d * 12.0; |
| 222 | for py in floor_start..self.screen_h { | 238 | for py in floor_start..self.screen_h { |
| 223 | let floor_dist = self.floor_dist_lut[py]; | 239 | let floor_dist = self.floor_dist_lut[py]; |
| 224 | let bright = 1.0 / (1.0 + floor_dist * 0.35 * darken_scale); | 240 | let bright = 1.0 / (1.0 + floor_dist * 0.35 * darken_scale); |
| @@ -226,7 +242,7 @@ impl Renderer { | |||
| 226 | 242 | ||
| 227 | if floor_dist > 15.0 { | 243 | if floor_dist > 15.0 { |
| 228 | let gray = darken_fx(0xFF80_8080, bright_fx); | 244 | let gray = darken_fx(0xFF80_8080, bright_fx); |
| 229 | buffer[py * self.screen_w + sx] = gray; | 245 | buffer[py * self.screen_w + sx] = color_drift_fx(gray, drift); |
| 230 | } else { | 246 | } else { |
| 231 | let fx = game.player.x + floor_dist * rdx_floor; | 247 | let fx = game.player.x + floor_dist * rdx_floor; |
| 232 | let fy = game.player.y + floor_dist * rdy_floor; | 248 | let fy = game.player.y + floor_dist * rdy_floor; |
| @@ -238,7 +254,8 @@ impl Renderer { | |||
| 238 | } else { | 254 | } else { |
| 239 | game.floor_texture[fty * TEX_SIZE + ftx] | 255 | game.floor_texture[fty * TEX_SIZE + ftx] |
| 240 | }; | 256 | }; |
| 241 | buffer[py * self.screen_w + sx] = darken_fx(color, bright_fx); | 257 | buffer[py * self.screen_w + sx] = |
| 258 | color_drift_fx(darken_fx(color, bright_fx), drift); | ||
| 242 | } | 259 | } |
| 243 | } | 260 | } |
| 244 | } | 261 | } |
| @@ -352,7 +369,8 @@ impl Renderer { | |||
| 352 | s.scale, | 369 | s.scale, |
| 353 | s.color, | 370 | s.color, |
| 354 | s.cam_z, | 371 | s.cam_z, |
| 355 | game.weakness_factor(), | 372 | game.darkness_factor(), |
| 373 | game.color_drift(), | ||
| 356 | wall_euclidean, | 374 | wall_euclidean, |
| 357 | ); | 375 | ); |
| 358 | } | 376 | } |
| @@ -418,6 +436,7 @@ impl Renderer { | |||
| 418 | color: u32, | 436 | color: u32, |
| 419 | cam_z: f64, | 437 | cam_z: f64, |
| 420 | weakness: f64, | 438 | weakness: f64, |
| 439 | drift: f64, | ||
| 421 | wall_euclidean: &[f64], | 440 | wall_euclidean: &[f64], |
| 422 | ) { | 441 | ) { |
| 423 | let glyph = match find_glyph(ch) { | 442 | let glyph = match find_glyph(ch) { |
| @@ -428,7 +447,7 @@ impl Renderer { | |||
| 428 | let darken_scale = 1.0 + weakness * 12.0; | 447 | let darken_scale = 1.0 + weakness * 12.0; |
| 429 | let bright = 1.0 / (1.0 + cam_z * 0.15 * darken_scale); | 448 | let bright = 1.0 / (1.0 + cam_z * 0.15 * darken_scale); |
| 430 | let bright_fx = (bright * 256.0) as u32; | 449 | let bright_fx = (bright * 256.0) as u32; |
| 431 | let final_color = darken_fx(color, bright_fx); | 450 | let final_color = color_drift_fx(darken_fx(color, bright_fx), drift); |
| 432 | 451 | ||
| 433 | let draw_w = (LETTER_W as f64 * scale) as isize; | 452 | let draw_w = (LETTER_W as f64 * scale) as isize; |
| 434 | let draw_h = (LETTER_H as f64 * scale) as isize; | 453 | let draw_h = (LETTER_H as f64 * scale) as isize; |
| @@ -707,3 +726,33 @@ fn darken_fx(color: u32, bright_fx: u32) -> u32 { | |||
| 707 | let b = ((color & 0xFF) * bright_fx) >> 8; | 726 | let b = ((color & 0xFF) * bright_fx) >> 8; |
| 708 | 0xFF00_0000 | (r << 16) | (g << 8) | b | 727 | 0xFF00_0000 | (r << 16) | (g << 8) | b |
| 709 | } | 728 | } |
| 729 | |||
| 730 | /// Subtle hue rotation for sanity-based colour drift. | ||
| 731 | /// `drift` is a small angle in radians; 0 means no change. | ||
| 732 | fn color_drift_fx(color: u32, drift: f64) -> u32 { | ||
| 733 | if drift.abs() < 0.001 { | ||
| 734 | return color; | ||
| 735 | } | ||
| 736 | let r = ((color >> 16) & 0xFF) as f64 / 255.0; | ||
| 737 | let g = ((color >> 8) & 0xFF) as f64 / 255.0; | ||
| 738 | let b = (color & 0xFF) as f64 / 255.0; | ||
| 739 | |||
| 740 | // Cheap hue shift: rotate in RGB space around the gray axis. | ||
| 741 | let gray = (r + g + b) / 3.0; | ||
| 742 | let dr = r - gray; | ||
| 743 | let dg = g - gray; | ||
| 744 | let db = b - gray; | ||
| 745 | |||
| 746 | let cos_d = drift.cos(); | ||
| 747 | let sin_d = drift.sin(); | ||
| 748 | |||
| 749 | // Rotate the RG plane (simple 2D projection for subtle effect). | ||
| 750 | let nr = gray + dr * cos_d - dg * sin_d * 0.5 + db * sin_d * 0.3; | ||
| 751 | let ng = gray + dg * cos_d + dr * sin_d * 0.5 - db * sin_d * 0.3; | ||
| 752 | let nb = gray + db * cos_d - dr * sin_d * 0.3 + dg * sin_d * 0.3; | ||
| 753 | |||
| 754 | let r = (nr.clamp(0.0, 1.0) * 255.0) as u32; | ||
| 755 | let g = (ng.clamp(0.0, 1.0) * 255.0) as u32; | ||
| 756 | let b = (nb.clamp(0.0, 1.0) * 255.0) as u32; | ||
| 757 | 0xFF00_0000 | (r << 16) | (g << 8) | b | ||
| 758 | } | ||
