summaryrefslogtreecommitdiffstats
path: root/src/renderer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer.rs')
-rw-r--r--src/renderer.rs404
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 @@
1use crate::game::Game; 1use crate::game::{Entity, Game, PickupType};
2use crate::texture::TEX_SIZE; 2use 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
7const 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
68const LETTER_W: usize = 5;
69const LETTER_H: usize = 5;
70
4pub struct Renderer { 71pub 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
13impl Renderer { 77impl 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
478fn 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
488fn 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
497fn 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
506fn 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)]
180fn darken_fx(color: u32, bright_fx: u32) -> u32 { 516fn 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;