| @@ -3,7 +3,7 @@ use bevy::prelude::*; |
| 3 |
3 |
use crate::{ |
| 4 |
4 |
sim::{SimSystems, SimTick, TICKS_PER_HOUR, hour_of_day}, |
| 5 |
5 |
structures::{Structure, StructureKind}, |
| 6 |
|
- world::{TilePos, WorldMap}, |
|
6 |
+ world::{TileChanged, TilePos, WorldMap}, |
| 7 |
7 |
}; |
| 8 |
8 |
|
| 9 |
9 |
pub struct LightPlugin; |
| @@ -20,6 +20,7 @@ pub const LIGHT_LOOKAHEAD_TICKS: u64 = TICKS_PER_HOUR * 3 / 2; |
| 20 |
20 |
const AMBIENT_STEPS: f32 = 16.0; |
| 21 |
21 |
const DAY_COLOR: Vec3 = Vec3::ONE; |
| 22 |
22 |
const NIGHT_COLOR: Vec3 = Vec3::new(0.2, 0.24, 0.4); |
|
23 |
+pub const ROOF_DAYLIGHT: f32 = 0.3; |
| 23 |
24 |
|
| 24 |
25 |
pub fn daylight(hour: f32) -> f32 { |
| 25 |
26 |
match hour { |
| @@ -55,6 +56,15 @@ pub fn light_at(kind: StructureKind, source: UVec2, tile: UVec2) -> Vec3 { |
| 55 |
56 |
color * (1.0 - d * d).powi(2) |
| 56 |
57 |
} |
| 57 |
58 |
|
|
59 |
+pub fn line_of_sight(map: &WorldMap, from: UVec2, to: UVec2) -> bool { |
|
60 |
+ let (a, b) = (from.as_vec2(), to.as_vec2()); |
|
61 |
+ let steps = (b - a).abs().max_element().ceil() as i32; |
|
62 |
+ (1..steps).all(|i| { |
|
63 |
+ let p = a.lerp(b, i as f32 / steps as f32).round().as_ivec2(); |
|
64 |
+ !map.is_wall(p) |
|
65 |
+ }) |
|
66 |
+} |
|
67 |
+ |
| 58 |
68 |
pub fn light_from(tile: UVec2, sources: impl IntoIterator<Item = (UVec2, StructureKind)>) -> Vec3 { |
| 59 |
69 |
sources |
| 60 |
70 |
.into_iter() |
| @@ -67,7 +77,9 @@ pub struct LightMap { |
| 67 |
77 |
size: UVec2, |
| 68 |
78 |
ambient_step: u32, |
| 69 |
79 |
ambient: Vec3, |
|
80 |
+ indoor_ambient: Vec3, |
| 70 |
81 |
sources: Vec<Vec3>, |
|
82 |
+ roofed: Vec<bool>, |
| 71 |
83 |
} |
| 72 |
84 |
|
| 73 |
85 |
impl LightMap { |
| @@ -75,7 +87,13 @@ impl LightMap { |
| 75 |
87 |
if self.sources.is_empty() || tile.x >= self.size.x || tile.y >= self.size.y { |
| 76 |
88 |
return Vec3::ONE; |
| 77 |
89 |
} |
| 78 |
|
- (self.ambient + self.sources[(tile.y * self.size.x + tile.x) as usize]).min(Vec3::ONE) |
|
90 |
+ let index = (tile.y * self.size.x + tile.x) as usize; |
|
91 |
+ let ambient = if self.roofed[index] { |
|
92 |
+ self.indoor_ambient |
|
93 |
+ } else { |
|
94 |
+ self.ambient |
|
95 |
+ }; |
|
96 |
+ (ambient + self.sources[index]).min(Vec3::ONE) |
| 79 |
97 |
} |
| 80 |
98 |
|
| 81 |
99 |
pub fn is_lit(&self, tile: UVec2) -> bool { |
| @@ -83,10 +101,11 @@ impl LightMap { |
| 83 |
101 |
} |
| 84 |
102 |
|
| 85 |
103 |
pub fn compute( |
| 86 |
|
- size: UVec2, |
|
104 |
+ map: &WorldMap, |
| 87 |
105 |
daylight: f32, |
| 88 |
106 |
sources: impl IntoIterator<Item = (UVec2, StructureKind)>, |
| 89 |
107 |
) -> Self { |
|
108 |
+ let size = map.size; |
| 90 |
109 |
let ambient_step = (daylight * AMBIENT_STEPS).round() as u32; |
| 91 |
110 |
let mut light = vec![Vec3::ZERO; size.element_product() as usize]; |
| 92 |
111 |
for (pos, kind) in sources { |
| @@ -105,15 +124,23 @@ impl LightMap { |
| 105 |
124 |
continue; |
| 106 |
125 |
} |
| 107 |
126 |
let tile = tile.as_uvec2(); |
| 108 |
|
- light[(tile.y * size.x + tile.x) as usize] += light_at(kind, pos, tile); |
|
127 |
+ if line_of_sight(map, pos, tile) { |
|
128 |
+ light[(tile.y * size.x + tile.x) as usize] += light_at(kind, pos, tile); |
|
129 |
+ } |
| 109 |
130 |
} |
| 110 |
131 |
} |
| 111 |
132 |
} |
|
133 |
+ let day = ambient_step as f32 / AMBIENT_STEPS; |
| 112 |
134 |
Self { |
| 113 |
135 |
size, |
| 114 |
136 |
ambient_step, |
| 115 |
|
- ambient: ambient_color(ambient_step as f32 / AMBIENT_STEPS), |
|
137 |
+ ambient: ambient_color(day), |
|
138 |
+ indoor_ambient: ambient_color(day * ROOF_DAYLIGHT), |
| 116 |
139 |
sources: light, |
|
140 |
+ roofed: map |
|
141 |
+ .positions() |
|
142 |
+ .map(|p| map.is_roofed(p.as_ivec2())) |
|
143 |
+ .collect(), |
| 117 |
144 |
} |
| 118 |
145 |
} |
| 119 |
146 |
} |
| @@ -124,25 +151,23 @@ fn update_light_map( |
| 124 |
151 |
structures: Query<(&TilePos, &Structure)>, |
| 125 |
152 |
added: Query<(), Added<Structure>>, |
| 126 |
153 |
mut removed: RemovedComponents<Structure>, |
|
154 |
+ mut changed: MessageReader<TileChanged>, |
| 127 |
155 |
mut light: ResMut<LightMap>, |
| 128 |
156 |
) { |
| 129 |
157 |
let day = daylight(hour_of_day(*tick)); |
| 130 |
158 |
let step = (day * AMBIENT_STEPS).round() as u32; |
| 131 |
|
- let sources_changed = !added.is_empty() || removed.read().count() > 0; |
|
159 |
+ let tiles_changed = changed.read().count() > 0; |
|
160 |
+ let sources_changed = !added.is_empty() || removed.read().count() > 0 || tiles_changed; |
| 132 |
161 |
if !sources_changed && step == light.ambient_step && light.size == map.size { |
| 133 |
162 |
return; |
| 134 |
163 |
} |
| 135 |
|
- *light = LightMap::compute( |
| 136 |
|
- map.size, |
| 137 |
|
- day, |
| 138 |
|
- structures.iter().map(|(pos, s)| (pos.0, s.kind)), |
| 139 |
|
- ); |
|
164 |
+ *light = LightMap::compute(&map, day, structures.iter().map(|(pos, s)| (pos.0, s.kind))); |
| 140 |
165 |
} |
| 141 |
166 |
|
| 142 |
167 |
#[cfg(test)] |
| 143 |
168 |
mod tests { |
| 144 |
169 |
use super::*; |
| 145 |
|
- use crate::sim::TICKS_PER_HOUR; |
|
170 |
+ use crate::{sim::TICKS_PER_HOUR, world::Terrain}; |
| 146 |
171 |
|
| 147 |
172 |
#[test] |
| 148 |
173 |
fn days_are_bright_and_nights_dark_with_gradual_dusk_and_dawn() { |
| @@ -178,14 +203,31 @@ mod tests { |
| 178 |
203 |
#[test] |
| 179 |
204 |
fn light_map_combines_ambient_and_fires() { |
| 180 |
205 |
let fire = (UVec2::new(5, 5), StructureKind::Campfire); |
| 181 |
|
- let night = LightMap::compute(UVec2::splat(20), 0.0, [fire]); |
|
206 |
+ let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
|
207 |
+ let night = LightMap::compute(&map, 0.0, [fire]); |
| 182 |
208 |
assert!(night.is_lit(UVec2::new(5, 6))); |
| 183 |
209 |
assert!(!night.is_lit(UVec2::new(18, 18))); |
| 184 |
|
- let day = LightMap::compute(UVec2::splat(20), 1.0, []); |
|
210 |
+ let day = LightMap::compute(&map, 1.0, []); |
| 185 |
211 |
assert!(day.is_lit(UVec2::new(18, 18))); |
| 186 |
212 |
} |
| 187 |
213 |
|
| 188 |
214 |
#[test] |
|
215 |
+ fn walls_cast_shadows_and_roofs_keep_daylight_out() { |
|
216 |
+ let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
|
217 |
+ for y in 0..20 { |
|
218 |
+ map.set_wall(UVec2::new(8, y), true); |
|
219 |
+ } |
|
220 |
+ map.set_roof(UVec2::new(15, 15), true); |
|
221 |
+ let fire = (UVec2::new(6, 10), StructureKind::Campfire); |
|
222 |
+ let night = LightMap::compute(&map, 0.0, [fire]); |
|
223 |
+ assert!(night.is_lit(UVec2::new(7, 10))); |
|
224 |
+ assert!(!night.is_lit(UVec2::new(9, 10))); |
|
225 |
+ let day = LightMap::compute(&map, 1.0, []); |
|
226 |
+ assert!(day.is_lit(UVec2::new(14, 15))); |
|
227 |
+ assert!(!day.is_lit(UVec2::new(15, 15))); |
|
228 |
+ } |
|
229 |
+ |
|
230 |
+ #[test] |
| 189 |
231 |
fn an_empty_light_map_counts_as_daylight() { |
| 190 |
232 |
assert!(LightMap::default().is_lit(UVec2::new(3, 3))); |
| 191 |
233 |
} |