use crate::coordinates::{ Distance, Neighbors };
use std::collections::HashSet;
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum FOVAlgorithm
{
Shadowcasting,
RayCasting,
FloodFill,
Bresenham,
}
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub struct VisibilityState
{
pub visible: bool,
pub distance: u32,
pub light_level: f32,
pub blocks_sight: bool,
}
impl VisibilityState
{
pub fn new( visible : bool, distance : u32, light_level : f32 ) -> Self
{
Self
{
visible,
distance,
light_level,
blocks_sight : false,
}
}
pub fn blocking( distance : u32, light_level : f32 ) -> Self
{
Self
{
visible : true, distance,
light_level,
blocks_sight : true,
}
}
pub fn invisible() -> Self
{
Self
{
visible : false,
distance : u32::MAX,
light_level : 0.0,
blocks_sight : false,
}
}
}
pub struct VisibilityMap< C >
{
visibility : std::collections::HashMap< C, VisibilityState >,
#[ allow( dead_code ) ]
viewer_position : C,
#[ allow( dead_code ) ]
view_range : u32,
}
impl< C > VisibilityMap< C >
where
C : Clone + std::hash::Hash + Eq,
{
pub fn new( viewer : C, range : u32 ) -> Self
{
Self
{
visibility : std::collections::HashMap::new(),
viewer_position : viewer,
view_range : range,
}
}
pub fn set_visibility( &mut self, coord : &C, state : VisibilityState )
{
self.visibility.insert( coord.clone(), state );
}
pub fn get_visibility( &self, coord : &C ) -> Option< &VisibilityState >
{
self.visibility.get( coord )
}
pub fn is_visible( &self, coord : &C ) -> bool
{
self.visibility.get( coord )
.map( | state | state.visible )
.unwrap_or( false )
}
pub fn distance_to( &self, coord : &C ) -> Option< u32 >
{
self.visibility.get( coord ).map( | state | state.distance )
}
pub fn light_level_at( &self, coord : &C ) -> f32
{
self.visibility.get( coord )
.map( | state | state.light_level )
.unwrap_or( 0.0 )
}
pub fn visible_coordinates( &self ) -> Vec< C >
{
self.visibility.iter()
.filter( | ( _, state ) | state.visible )
.map( | ( coord, _ ) | coord.clone() )
.collect()
}
pub fn coordinates_in_range( &self, min_dist : u32, max_dist : u32 ) -> Vec< C >
{
self.visibility.iter()
.filter( | ( _, state ) |
{
state.visible && state.distance >= min_dist && state.distance <= max_dist
})
.map( | ( coord, _ ) | coord.clone() )
.collect()
}
pub fn visible_positions( &self ) -> impl Iterator< Item = C > + '_
{
self.visibility.iter()
.filter_map( | ( coord, state ) |
{
if state.visible
{
Some( coord.clone() )
}
else
{
None
}
})
}
}
pub struct FieldOfView
{
algorithm : FOVAlgorithm,
include_viewer : bool,
}
impl FieldOfView
{
pub fn new() -> Self
{
Self
{
algorithm : FOVAlgorithm::Shadowcasting,
include_viewer : true,
}
}
pub fn with_algorithm( algorithm : FOVAlgorithm ) -> Self
{
Self
{
algorithm,
include_viewer : true,
}
}
pub fn include_viewer( mut self, include : bool ) -> Self
{
self.include_viewer = include;
self
}
pub fn calculate_fov< C, F >
(
&self,
viewer : &C,
max_range : u32,
blocks_sight : F
) -> VisibilityMap< C >
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
F : Fn( &C ) -> bool,
{
let mut visibility_map = VisibilityMap::new( viewer.clone(), max_range );
match self.algorithm
{
FOVAlgorithm::Shadowcasting =>
{
self.calculate_shadowcasting_fov( viewer, max_range, &blocks_sight, &mut visibility_map );
}
FOVAlgorithm::RayCasting =>
{
self.calculate_ray_casting_fov( viewer, max_range, &blocks_sight, &mut visibility_map );
}
FOVAlgorithm::FloodFill =>
{
self.calculate_flood_fill_fov( viewer, max_range, &blocks_sight, &mut visibility_map );
}
FOVAlgorithm::Bresenham =>
{
self.calculate_bresenham_fov( viewer, max_range, &blocks_sight, &mut visibility_map );
}
}
if self.include_viewer
{
visibility_map.set_visibility( viewer, VisibilityState::new( true, 0, 1.0 ) );
}
visibility_map
}
pub fn line_of_sight< C, F >( &self, from : &C, to : &C, blocks_sight : F ) -> bool
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
F : Fn( &C ) -> bool,
{
let distance = from.distance( to ) as u32;
let visibility = self.calculate_fov( from, distance + 1, blocks_sight );
visibility.is_visible( to )
}
fn calculate_shadowcasting_fov< C, F >
(
&self,
viewer : &C,
max_range : u32,
blocks_sight : &F,
visibility_map : &mut VisibilityMap< C >
)
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
F : Fn( &C ) -> bool,
{
let neighbors = viewer.neighbors();
let neighbor_count = neighbors.len();
for i in 0..neighbor_count
{
self.cast_octant_shadows( viewer, max_range, blocks_sight, visibility_map, i, neighbor_count );
}
}
fn cast_octant_shadows< C, F >
(
&self,
viewer : &C,
max_range : u32,
blocks_sight : &F,
visibility_map : &mut VisibilityMap< C >,
octant : usize,
total_directions : usize,
)
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
F : Fn( &C ) -> bool,
{
let mut current_positions = vec![ viewer.clone() ];
for _distance in 1..=max_range
{
let mut next_positions = Vec::new();
let mut blocked_positions = HashSet::new();
for pos in ¤t_positions
{
let neighbors = pos.neighbors();
for ( i, neighbor ) in neighbors.iter().enumerate()
{
if ( i + total_directions - octant ) % total_directions < 3 ||
( i + total_directions - octant ) % total_directions > total_directions - 3
{
let actual_distance = viewer.distance( neighbor ) as u32;
if actual_distance <= max_range
{
let light_level = ( 1.0f32 - ( actual_distance as f32 / max_range as f32 ) ).max( 0.0f32 );
let is_blocked = blocks_sight( neighbor );
let visibility_state = if is_blocked
{
blocked_positions.insert( neighbor.clone() );
VisibilityState::blocking( actual_distance, light_level )
}
else
{
VisibilityState::new( true, actual_distance, light_level )
};
visibility_map.set_visibility( neighbor, visibility_state );
if !is_blocked
{
next_positions.push( neighbor.clone() );
}
}
}
}
}
current_positions = next_positions.into_iter()
.filter( | pos | !blocked_positions.contains( pos ) )
.collect();
if current_positions.is_empty()
{
break;
}
}
}
fn calculate_ray_casting_fov<C, F>(
&self,
viewer: &C,
max_range: u32,
blocks_sight: &F,
visibility_map: &mut VisibilityMap<C>
)
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
F: Fn(&C) -> bool,
{
let neighbors = viewer.neighbors();
for start_neighbor in neighbors
{
self.cast_directional_ray(viewer, &start_neighbor, max_range, blocks_sight, visibility_map);
}
let neighbor_list = viewer.neighbors();
for i in 0..neighbor_list.len()
{
for j in (i + 1)..neighbor_list.len()
{
if let Some(diagonal_target) = self.find_diagonal_target(viewer, &neighbor_list[i], &neighbor_list[j], max_range)
{
self.cast_directional_ray(viewer, &diagonal_target, max_range, blocks_sight, visibility_map);
}
}
}
}
fn cast_directional_ray<C, F>(
&self,
viewer: &C,
direction_target: &C,
max_range: u32,
blocks_sight: &F,
visibility_map: &mut VisibilityMap<C>
)
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
F: Fn(&C) -> bool,
{
let mut current = viewer.clone();
let mut distance = 0u32;
while distance < max_range
{
let neighbors = current.neighbors();
let mut best_next = None;
let mut best_alignment = f32::MIN;
for neighbor in neighbors
{
let alignment = self.calculate_direction_alignment(viewer, direction_target, ¤t, &neighbor);
if alignment > best_alignment
{
best_alignment = alignment;
best_next = Some(neighbor);
}
}
if let Some(next) = best_next
{
current = next;
distance = viewer.distance(¤t) as u32;
if distance > max_range
{
break;
}
let light_level = (1.0f32 - (distance as f32 / max_range as f32)).max(0.0f32);
let is_blocked = blocks_sight(¤t);
let visibility_state = if is_blocked
{
VisibilityState::blocking(distance, light_level)
}
else
{
VisibilityState::new(true, distance, light_level)
};
visibility_map.set_visibility(¤t, visibility_state);
if is_blocked
{
break; }
}
else
{
break; }
}
}
fn calculate_direction_alignment<C>(
&self,
viewer: &C,
direction_target: &C,
current: &C,
next: &C,
) -> f32
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
{
let target_distance = viewer.distance(direction_target) as f32;
let current_distance = viewer.distance(current) as f32;
let next_distance = viewer.distance(next) as f32;
let target_to_next = direction_target.distance(next) as f32;
if target_distance == 0.0 || current_distance == 0.0
{
return 0.0;
}
let progress = (next_distance - current_distance) / target_distance;
let deviation_penalty = target_to_next / (target_distance + 1.0);
progress - deviation_penalty
}
fn find_diagonal_target<C>(
&self,
viewer: &C,
neighbor1: &C,
neighbor2: &C,
max_range: u32,
) -> Option<C>
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
{
let neighbors1 = neighbor1.neighbors();
let neighbors2 = neighbor2.neighbors();
for n1 in &neighbors1
{
for n2 in &neighbors2
{
if n1 == n2 && viewer.distance(n1) <= max_range
{
return Some(n1.clone());
}
}
}
None
}
fn calculate_flood_fill_fov<C, F>(
&self,
viewer: &C,
max_range: u32,
blocks_sight: &F,
visibility_map: &mut VisibilityMap<C>
)
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
F: Fn(&C) -> bool,
{
let mut visited = HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((viewer.clone(), 0));
while let Some((current_pos, distance)) = queue.pop_front() {
if visited.contains(¤t_pos) || distance > max_range {
continue;
}
visited.insert(current_pos.clone());
let light_level = (1.0f32 - (distance as f32 / max_range as f32)).max(0.0f32);
let is_blocked = blocks_sight(¤t_pos);
let visibility_state = if is_blocked {
VisibilityState::blocking(distance, light_level)
} else {
VisibilityState::new(true, distance, light_level)
};
visibility_map.set_visibility(¤t_pos, visibility_state);
if !is_blocked && distance < max_range {
for neighbor_coord in current_pos.neighbors() {
if !visited.contains(&neighbor_coord) {
queue.push_back((neighbor_coord, distance + 1));
}
}
}
}
}
fn calculate_bresenham_fov<C, F>(
&self,
viewer: &C,
max_range: u32,
blocks_sight: &F,
visibility_map: &mut VisibilityMap<C>
)
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
F: Fn(&C) -> bool,
{
let mut all_positions = HashSet::new();
let mut visited = HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((viewer.clone(), 0));
while let Some((current_pos, distance)) = queue.pop_front() {
if visited.contains(¤t_pos) || distance > max_range {
continue;
}
visited.insert(current_pos.clone());
all_positions.insert(current_pos.clone());
for neighbor_coord in current_pos.neighbors() {
if !visited.contains(&neighbor_coord) {
queue.push_back((neighbor_coord, distance + 1));
}
}
}
for target in all_positions {
let distance = viewer.distance(&target) as u32;
let has_line_of_sight = self.check_bresenham_line(viewer, &target, blocks_sight);
if has_line_of_sight {
let light_level = (1.0f32 - (distance as f32 / max_range as f32)).max(0.0f32);
let is_blocked = blocks_sight(&target);
let visibility_state = if is_blocked {
VisibilityState::blocking(distance, light_level)
} else {
VisibilityState::new(true, distance, light_level)
};
visibility_map.set_visibility(&target, visibility_state);
}
}
}
fn check_bresenham_line<C, F>(&self, from: &C, to: &C, blocks_sight: &F) -> bool
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
F: Fn(&C) -> bool,
{
let line_positions = self.trace_bresenham_line(from, to);
for pos in line_positions.iter().skip(1) {
if pos == to
{
break; }
if blocks_sight(pos)
{
return false; }
}
true }
fn trace_bresenham_line<C>(&self, from: &C, to: &C) -> Vec<C>
where
C: Distance + Neighbors + Clone + std::hash::Hash + Eq,
{
let mut line_positions = Vec::new();
let mut current = from.clone();
line_positions.push(current.clone());
while current != *to
{
let neighbors = current.neighbors();
let mut best_neighbor = None;
let mut best_distance = u32::MAX;
for neighbor in neighbors
{
let distance_to_target = neighbor.distance(to);
if distance_to_target < best_distance
{
best_distance = distance_to_target;
best_neighbor = Some(neighbor);
}
}
if let Some(next) = best_neighbor
{
if next == current
{
break; }
current = next;
line_positions.push(current.clone());
if line_positions.len() > 1000
{
break;
}
}
else
{
break; }
}
line_positions
}
}
impl Default for FieldOfView
{
fn default() -> Self
{
Self::new()
}
}
#[ derive( Debug, Clone ) ]
pub struct LightSource< C >
{
pub position : C,
pub radius : u32,
pub intensity : f32,
pub color : ( f32, f32, f32 ),
pub penetrates_walls : bool,
}
impl< C > LightSource< C >
{
pub fn new( position : C, radius : u32, intensity : f32 ) -> Self
{
Self
{
position,
radius,
intensity,
color : ( 1.0, 1.0, 1.0 ), penetrates_walls : false,
}
}
pub fn with_color( mut self, r : f32, g : f32, b : f32 ) -> Self
{
self.color = ( r, g, b );
self
}
pub fn penetrating( mut self, penetrates : bool ) -> Self
{
self.penetrates_walls = penetrates;
self
}
}
pub struct LightingCalculator< C >
{
light_sources : Vec< LightSource< C > >,
fov_calculator : FieldOfView,
}
impl< C > LightingCalculator< C >
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
{
pub fn new() -> Self
{
Self
{
light_sources : Vec::new(),
fov_calculator : FieldOfView::new(),
}
}
pub fn add_light_source( &mut self, light : LightSource< C > )
{
self.light_sources.push( light );
}
pub fn calculate_lighting< F >( &self, blocks_sight : F ) -> std::collections::HashMap< C, f32 >
where
F : Fn( &C ) -> bool,
{
let mut lighting_map = std::collections::HashMap::new();
for light_source in &self.light_sources
{
let visibility_map = if light_source.penetrates_walls
{
self.fov_calculator.calculate_fov( &light_source.position, light_source.radius, | _ | false )
}
else
{
self.fov_calculator.calculate_fov( &light_source.position, light_source.radius, &blocks_sight )
};
for coord in visibility_map.visible_coordinates()
{
let distance = light_source.position.distance( &coord ) as f32;
let light_falloff = ( 1.0f32 - ( distance / light_source.radius as f32 ) ).max( 0.0f32 );
let light_contribution = light_source.intensity * light_falloff;
let current_light = lighting_map.get( &coord ).unwrap_or( &0.0 );
lighting_map.insert( coord, ( current_light + light_contribution ).min( 1.0 ) );
}
}
lighting_map
}
}
impl< C > Default for LightingCalculator< C >
where
C : Distance + Neighbors + Clone + std::hash::Hash + Eq,
{
fn default() -> Self
{
Self::new()
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
use crate::coordinates::square::{ Coordinate as SquareCoord, EightConnected };
#[ test ]
fn test_visibility_state_creation()
{
let visible_state = VisibilityState::new( true, 5, 0.8 );
assert!( visible_state.visible );
assert_eq!( visible_state.distance, 5 );
assert_eq!( visible_state.light_level, 0.8 );
assert!( !visible_state.blocks_sight );
let blocking_state = VisibilityState::blocking( 3, 0.5 );
assert!( blocking_state.visible );
assert!( blocking_state.blocks_sight );
let invisible_state = VisibilityState::invisible();
assert!( !invisible_state.visible );
assert_eq!( invisible_state.light_level, 0.0 );
}
#[ test ]
fn test_visibility_map_basic()
{
let viewer = SquareCoord::< EightConnected >::new( 0, 0 );
let mut visibility_map = VisibilityMap::new( viewer.clone(), 10 );
let target = SquareCoord::< EightConnected >::new( 3, 3 );
visibility_map.set_visibility( &target, VisibilityState::new( true, 5, 0.7 ) );
assert!( visibility_map.is_visible( &target ) );
assert_eq!( visibility_map.distance_to( &target ), Some( 5 ) );
assert_eq!( visibility_map.light_level_at( &target ), 0.7 );
}
#[ test ]
fn test_fov_calculator_creation()
{
let fov = FieldOfView::new();
assert_eq!( fov.algorithm, FOVAlgorithm::Shadowcasting );
assert!( fov.include_viewer );
let ray_fov = FieldOfView::with_algorithm( FOVAlgorithm::RayCasting )
.include_viewer( false );
assert_eq!( ray_fov.algorithm, FOVAlgorithm::RayCasting );
assert!( !ray_fov.include_viewer );
}
#[ test ]
fn test_fov_calculation_basic()
{
let fov = FieldOfView::new();
let viewer = SquareCoord::< EightConnected >::new( 5, 5 );
let visibility = fov.calculate_fov( &viewer, 3, | _ | false );
assert!( visibility.is_visible( &viewer ) );
assert_eq!( visibility.distance_to( &viewer ), Some( 0 ) );
}
#[ test ]
fn test_line_of_sight()
{
let fov = FieldOfView::new();
let from = SquareCoord::< EightConnected >::new( 0, 0 );
let to = SquareCoord::< EightConnected >::new( 1, 1 );
let has_los = fov.line_of_sight( &from, &to, | _ | false );
println!( "Line of sight result: {}", has_los );
let _blocked_los = fov.line_of_sight( &from, &to, | _ | true );
}
#[ test ]
fn test_light_source_creation()
{
let position = SquareCoord::< EightConnected >::new( 10, 10 );
let light = LightSource::new( position.clone(), 8, 0.9 )
.with_color( 1.0, 0.8, 0.6 )
.penetrating( true );
assert_eq!( light.radius, 8 );
assert_eq!( light.intensity, 0.9 );
assert_eq!( light.color, ( 1.0, 0.8, 0.6 ) );
assert!( light.penetrates_walls );
}
#[ test ]
fn test_lighting_calculator()
{
let mut calculator = LightingCalculator::new();
let light_pos = SquareCoord::< EightConnected >::new( 5, 5 );
let light_source = LightSource::new( light_pos, 5, 1.0 );
calculator.add_light_source( light_source );
let lighting = calculator.calculate_lighting( | _ | false );
assert!( !lighting.is_empty() );
}
}