Files
Verbatim/src/render/lighting.rs
T
Emil 0b20f3ef9b fix: 15+ bug fixes — item dup, descend, slime AI, combat, camera, UI
CRITICAL:
- Fix use_item duplicating weapons/armor (not removed from inventory)
- Fix unwrap() panic in update_ragdoll_entity
- Fix descend proceeding without stairs when player entity missing

HIGH:
- Fix projectile hardcoded id==0 preventing enemy projectiles hitting player
- Fix previously equipped item lost on re-equip (returned to inventory)
- Fix i32::abs() overflow panic in background_color (use wrapping_abs)

MEDIUM:
- Fix slime AI ignoring vertical direction to player
- Fix combat damage applied to dead player
- Fix camera movement keys non-functional (added cam_offset_x/y)
- Fix Unicode emoji status icons (replaced with ASCII F/P/I/B)
- Fix Unicode UI chars (█░·─│┌┐└┘◆■ → ASCII #-./|++++*#)
- Add missing char_bitmap glyphs (+, =, >, <, *, %, #, |, @, _)

LOW:
- Fix draw_messages writing to negative y coordinates
- Fix WindowInput losing key state on focus loss (clear_keys on Focused(false))
- Fix compute_lighting using full grid scan instead of gather_sources_in_range

All 171 tests + 14 scenarios pass.
2026-06-21 16:24:51 +03:00

284 lines
7.8 KiB
Rust

use crate::world::cell::MaterialId;
use crate::world::grid::Grid;
#[derive(Clone, Copy, Debug)]
pub struct LightSource {
pub x: i32,
pub y: i32,
pub color: [u8; 3],
pub intensity: f32,
pub radius: u32,
}
pub struct LightGrid {
pub width: usize,
pub height: usize,
pub data: Vec<[u8; 3]>,
}
impl LightGrid {
pub fn new(width: usize, height: usize) -> Self {
let data = vec![[0; 3]; width * height];
Self {
width,
height,
data,
}
}
pub fn get(&self, x: i32, y: i32) -> [u8; 3] {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
return [0, 0, 0];
}
self.data[y as usize * self.width + x as usize]
}
pub fn set(&mut self, x: i32, y: i32, value: [u8; 3]) {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
return;
}
self.data[y as usize * self.width + x as usize] = value;
}
pub fn clear(&mut self, ambient: [u8; 3]) {
for v in self.data.iter_mut() {
*v = ambient;
}
}
}
pub fn material_light(material: MaterialId) -> Option<LightSource> {
match material {
MaterialId::Lava => Some(LightSource {
x: 0,
y: 0,
color: [255, 80, 20],
intensity: 1.0,
radius: 24,
}),
MaterialId::Fire => Some(LightSource {
x: 0,
y: 0,
color: [255, 160, 40],
intensity: 1.0,
radius: 18,
}),
_ => None,
}
}
pub fn gather_sources(grid: &Grid) -> Vec<LightSource> {
let mut sources = Vec::new();
let w = grid.width;
let h = grid.height;
for y in 0..h {
for x in 0..w {
let cell = grid.get(x as i32, y as i32);
if let Some(mut src) = material_light(cell.material) {
src.x = x as i32;
src.y = y as i32;
sources.push(src);
}
}
}
sources
}
pub fn gather_sources_in_range(
grid: &Grid,
cam_x: i32,
cam_y: i32,
view_w: usize,
view_h: usize,
margin: i32,
) -> Vec<LightSource> {
let mut sources = Vec::new();
let min_x = (cam_x - margin).max(0);
let max_x = (cam_x + view_w as i32 + margin).min(grid.width as i32);
let min_y = (cam_y - margin).max(0);
let max_y = (cam_y + view_h as i32 + margin).min(grid.height as i32);
for y in min_y..max_y {
for x in min_x..max_x {
let cell = grid.get(x, y);
if let Some(mut src) = material_light(cell.material) {
src.x = x;
src.y = y;
sources.push(src);
}
}
}
sources
}
pub fn compute_lighting(
grid: &Grid,
cam_x: i32,
cam_y: i32,
view_w: usize,
view_h: usize,
ambient: [u8; 3],
) -> LightGrid {
let mut grid_light = LightGrid::new(view_w, view_h);
grid_light.clear(ambient);
let sources = gather_sources_in_range(grid, cam_x, cam_y, view_w, view_h, 30);
let cap = sources.len().min(32);
let radius_limit = 30u32;
for src in sources.iter().take(cap) {
let r = src.radius.min(radius_limit) as i32;
let r2 = r * r;
let sx = src.x;
let sy = src.y;
for dy in -r..=r {
for dx in -r..=r {
let d2 = dx * dx + dy * dy;
if d2 > r2 {
continue;
}
let tx = sx + dx;
let ty = sy + dy;
if !grid.in_bounds(tx, ty) {
continue;
}
let vx = tx - cam_x;
let vy = ty - cam_y;
if vx < 0 || vx >= view_w as i32 || vy < 0 || vy >= view_h as i32 {
continue;
}
if !line_of_sight(grid, sx, sy, tx, ty) {
continue;
}
let dist = (d2 as f32).sqrt();
let t = 1.0 - (dist / r as f32);
if t <= 0.0 {
continue;
}
let attenuation = t * t;
let contrib = [
(src.color[0] as f32 * src.intensity * attenuation),
(src.color[1] as f32 * src.intensity * attenuation),
(src.color[2] as f32 * src.intensity * attenuation),
];
let idx = vy as usize * view_w + vx as usize;
let cur = grid_light.data[idx];
let next = [
(cur[0] as f32 + contrib[0]).min(255.0) as u8,
(cur[1] as f32 + contrib[1]).min(255.0) as u8,
(cur[2] as f32 + contrib[2]).min(255.0) as u8,
];
grid_light.data[idx] = next;
}
}
}
grid_light
}
pub fn line_of_sight(grid: &Grid, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
let mut x = x0;
let mut y = y0;
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx - dy;
loop {
if x == x1 && y == y1 {
return true;
}
if grid.in_bounds(x, y) && grid.get(x, y).is_solid() {
return false;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x += sx;
}
if e2 < dx {
err += dx;
y += sy;
}
}
}
pub fn apply_light(color: [u8; 3], light: [u8; 3]) -> [u8; 3] {
[
((color[0] as f32 * light[0] as f32 / 255.0).min(255.0) as u8),
((color[1] as f32 * light[1] as f32 / 255.0).min(255.0) as u8),
((color[2] as f32 * light[2] as f32 / 255.0).min(255.0) as u8),
]
}
pub fn apply_light_rgba(color: [u8; 4], light: [u8; 3]) -> [u8; 4] {
[
((color[0] as f32 * light[0] as f32 / 255.0).min(255.0) as u8),
((color[1] as f32 * light[1] as f32 / 255.0).min(255.0) as u8),
((color[2] as f32 * light[2] as f32 / 255.0).min(255.0) as u8),
color[3],
]
}
pub fn apply_light_tuple(color: (u8, u8, u8), light: [u8; 3]) -> (u8, u8, u8) {
(
((color.0 as f32 * light[0] as f32 / 255.0).min(255.0) as u8),
((color.1 as f32 * light[1] as f32 / 255.0).min(255.0) as u8),
((color.2 as f32 * light[2] as f32 / 255.0).min(255.0) as u8),
)
}
pub fn ambient_light() -> [u8; 3] {
[160, 160, 180]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::grid::Grid;
fn grid_with_lava() -> (Grid, i32, i32) {
let mut grid = Grid::new();
grid.set_material(10, 10, MaterialId::Lava);
(grid, 10, 10)
}
#[test]
fn lava_emits_light() {
let (grid, x, y) = grid_with_lava();
let sources = gather_sources(&grid);
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].x, x);
assert_eq!(sources[0].y, y);
}
#[test]
fn light_attenuates_with_distance() {
let (grid, _, _) = grid_with_lava();
let light = compute_lighting(&grid, 0, 0, 20, 20, ambient_light());
let center = light.get(10, 10);
let far = light.get(10, 0);
assert!(
center.iter().map(|&v| v as u32).sum::<u32>()
> far.iter().map(|&v| v as u32).sum::<u32>()
);
}
#[test]
fn walls_block_light() {
let mut grid = Grid::new();
grid.set_material(5, 10, MaterialId::Lava);
for y in 7..13 {
grid.set_material(8, y, MaterialId::Stone);
}
let light = compute_lighting(&grid, 0, 0, 20, 20, ambient_light());
let lit_side = light.get(6, 10);
let shadow_side = light.get(10, 10);
assert!(
lit_side.iter().map(|&v| v as u32).sum::<u32>()
> shadow_side.iter().map(|&v| v as u32).sum::<u32>()
);
}
}