src/rooms.rs 12.5 K · 380 lines · raw · history

1 use std::collections::{HashMap, HashSet, VecDeque};
2
3 use bevy::prelude::*;
4
5 use crate::{
6 building::{Blueprints, BuildKind, MAX_ROOM_TILES},
7 colonist::Colonist,
8 sim::{SimSystems, SimTick},
9 structures::{Structure, StructureKind},
10 world::{Terrain, TilePos, WorldMap},
11 worldgen::WorldGenerated,
12 };
13
14 pub struct RoomsPlugin;
15
16 impl Plugin for RoomsPlugin {
17 fn build(&self, app: &mut App) {
18 app.init_resource::<Rooms>()
19 .add_systems(Update, clear_rooms.run_if(on_message::<WorldGenerated>))
20 .add_systems(
21 FixedUpdate,
22 (update_rooms, (roof_rooms, remove_bad_doors))
23 .chain()
24 .in_set(SimSystems::Decide),
25 );
26 }
27 }
28
29 pub const MIN_ROOM_TILES: usize = 4;
30 const ROOM_CHECK_TICKS: u64 = 60;
31 const ROOM_PLAN_TICKS: u64 = 300;
32 const CROWDED_TILES_PER_BED: usize = 3;
33 const SIDES: [IVec2; 4] = [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y];
34
35 #[derive(Debug, Clone, PartialEq)]
36 pub struct Room {
37 pub tiles: Vec<UVec2>,
38 pub doors: Vec<UVec2>,
39 pub quality: f32,
40 }
41
42 impl Room {
43 pub fn bounds(&self) -> (UVec2, UVec2) {
44 let lo = self.tiles.iter().fold(UVec2::MAX, |a, t| a.min(*t));
45 let hi = self.tiles.iter().fold(UVec2::ZERO, |a, t| a.max(*t));
46 (lo, hi)
47 }
48
49 pub fn is_rectangle(&self) -> bool {
50 let (lo, hi) = self.bounds();
51 (hi - lo + UVec2::ONE).element_product() as usize == self.tiles.len()
52 }
53 }
54
55 #[derive(Resource, Default, Debug)]
56 pub struct Rooms {
57 pub rooms: Vec<Room>,
58 by_tile: HashMap<UVec2, usize>,
59 shape: Option<(u64, Vec<UVec2>)>,
60 }
61
62 impl Rooms {
63 pub fn at(&self, tile: UVec2) -> Option<&Room> {
64 self.by_tile.get(&tile).map(|&i| &self.rooms[i])
65 }
66
67 pub fn contains(&self, tile: UVec2) -> bool {
68 self.by_tile.contains_key(&tile)
69 }
70
71 pub fn detect(map: &WorldMap, doors: &HashSet<UVec2>) -> Self {
72 let detected = detect_rooms(map, doors);
73 Self {
74 by_tile: detected
75 .iter()
76 .enumerate()
77 .flat_map(|(i, (tiles, _))| tiles.iter().map(move |t| (*t, i)))
78 .collect(),
79 rooms: detected
80 .into_iter()
81 .map(|(tiles, doors)| Room {
82 tiles,
83 doors,
84 quality: 0.0,
85 })
86 .collect(),
87 shape: None,
88 }
89 }
90 }
91
92 fn clear_rooms(mut rooms: ResMut<Rooms>) {
93 *rooms = Rooms::default();
94 }
95
96 pub fn blocks_room(map: &WorldMap, doors: &HashSet<UVec2>, tile: IVec2) -> bool {
97 !map.in_bounds(tile)
98 || map.is_wall(tile)
99 || doors.contains(&tile.as_uvec2())
100 || map.get(tile).is_none_or(|t| t.move_cost().is_none())
101 }
102
103 pub fn detect_rooms(map: &WorldMap, doors: &HashSet<UVec2>) -> Vec<(Vec<UVec2>, Vec<UVec2>)> {
104 let width = map.size.x;
105 let index = |p: UVec2| (p.y * width + p.x) as usize;
106 let mut seen = vec![false; map.size.element_product() as usize];
107 let mut rooms = Vec::new();
108 for start in map.positions() {
109 if seen[index(start)] || blocks_room(map, doors, start.as_ivec2()) {
110 continue;
111 }
112 seen[index(start)] = true;
113 let mut region = vec![start];
114 let mut room_doors = HashSet::new();
115 let mut outdoors = false;
116 let mut queue = VecDeque::from([start]);
117 while let Some(tile) = queue.pop_front() {
118 for side in SIDES {
119 let next = tile.as_ivec2() + side;
120 if !map.in_bounds(next) {
121 outdoors = true;
122 continue;
123 }
124 let next_u = next.as_uvec2();
125 if doors.contains(&next_u) {
126 room_doors.insert(next_u);
127 }
128 if blocks_room(map, doors, next) || seen[index(next_u)] {
129 continue;
130 }
131 seen[index(next_u)] = true;
132 region.push(next_u);
133 queue.push_back(next_u);
134 }
135 }
136 let size = region.len();
137 if !outdoors && size >= MIN_ROOM_TILES && size <= MAX_ROOM_TILES && !room_doors.is_empty() {
138 region.sort_by_key(|t| (t.y, t.x));
139 let mut room_doors: Vec<UVec2> = room_doors.into_iter().collect();
140 room_doors.sort_by_key(|t| (t.y, t.x));
141 rooms.push((region, room_doors));
142 }
143 }
144 rooms
145 }
146
147 #[derive(Debug, Default, Clone, Copy)]
148 pub struct Furnishing {
149 pub beds: usize,
150 pub statue: bool,
151 pub hearth: bool,
152 }
153
154 pub fn room_quality(map: &WorldMap, tiles: &[UVec2], furnishing: Furnishing) -> f32 {
155 let n = tiles.len().max(1) as f32;
156 let share =
157 |test: &dyn Fn(UVec2) -> bool| tiles.iter().filter(|t| test(**t)).count() as f32 / n;
158 let floor = share(&|t| map.get(t.as_ivec2()) == Some(Terrain::WoodFloor));
159 let roofed = share(&|t| map.is_roofed(t.as_ivec2()));
160 let space = (n / 16.0).min(1.0);
161 let crowded = furnishing.beds * CROWDED_TILES_PER_BED > tiles.len();
162 let quality = 0.1
163 + 0.2 * space
164 + 0.25 * floor
165 + 0.15 * roofed
166 + if furnishing.statue { 0.2 } else { 0.0 }
167 + if furnishing.hearth { 0.1 } else { 0.0 }
168 - if crowded { 0.2 } else { 0.0 };
169 quality.clamp(0.0, 1.0)
170 }
171
172 #[allow(clippy::type_complexity)]
173 fn update_rooms(
174 tick: Res<SimTick>,
175 map: Res<WorldMap>,
176 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
177 mut rooms: ResMut<Rooms>,
178 ) {
179 if !tick.0.is_multiple_of(ROOM_CHECK_TICKS) {
180 return;
181 }
182 let mut doors: Vec<UVec2> = structures
183 .iter()
184 .filter(|(_, s)| s.kind == StructureKind::Door)
185 .map(|(pos, _)| pos.0)
186 .collect();
187 doors.sort_by_key(|t| (t.y, t.x));
188 let shape = (map.shape_version(), doors.clone());
189 if rooms.shape.as_ref() != Some(&shape) {
190 let door_set: HashSet<UVec2> = doors.into_iter().collect();
191 *rooms = Rooms::detect(&map, &door_set);
192 rooms.shape = Some(shape);
193 } else if !tick.0.is_multiple_of(ROOM_PLAN_TICKS) {
194 return;
195 }
196 let mut furnishing = vec![Furnishing::default(); rooms.rooms.len()];
197 for (pos, structure) in &structures {
198 let Some(&i) = rooms.by_tile.get(&pos.0) else {
199 continue;
200 };
201 match structure.kind {
202 StructureKind::Bed => furnishing[i].beds += 1,
203 StructureKind::Statue => furnishing[i].statue = true,
204 StructureKind::Hearth => furnishing[i].hearth = true,
205 _ => {}
206 }
207 }
208 for (room, furnishing) in rooms.rooms.iter_mut().zip(furnishing) {
209 room.quality = room_quality(&map, &room.tiles, furnishing);
210 }
211 }
212
213 fn roof_rooms(
214 tick: Res<SimTick>,
215 map: Res<WorldMap>,
216 rooms: Res<Rooms>,
217 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
218 mut blueprints: ResMut<Blueprints>,
219 ) {
220 if !tick.0.is_multiple_of(ROOM_PLAN_TICKS) {
221 return;
222 }
223 let lived_in: HashSet<*const Room> = structures
224 .iter()
225 .filter(|(_, s)| matches!(s.kind, StructureKind::Bed | StructureKind::Hearth))
226 .filter_map(|(pos, _)| rooms.at(pos.0))
227 .map(|room| room as *const Room)
228 .collect();
229 for room in &rooms.rooms {
230 if !lived_in.contains(&(room as *const Room)) {
231 continue;
232 }
233 for &tile in &room.tiles {
234 if !map.is_roofed(tile.as_ivec2()) && blueprints.get(tile).is_none() {
235 blueprints.0.insert(tile, BuildKind::Roof);
236 }
237 }
238 }
239 }
240
241 pub fn bad_door(map: &WorldMap, door: UVec2) -> bool {
242 let blocked = SIDES
243 .iter()
244 .filter(|side| {
245 let next = door.as_ivec2() + **side;
246 !map.in_bounds(next) || map.move_cost(next).is_none()
247 })
248 .count();
249 blocked >= 3
250 }
251
252 fn remove_bad_doors(
253 tick: Res<SimTick>,
254 map: Res<WorldMap>,
255 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
256 mut blueprints: ResMut<Blueprints>,
257 ) {
258 if !tick.0.is_multiple_of(ROOM_PLAN_TICKS) {
259 return;
260 }
261 for (pos, structure) in &structures {
262 if structure.kind == StructureKind::Door
263 && bad_door(&map, pos.0)
264 && blueprints.get(pos.0).is_none()
265 {
266 blueprints.0.insert(pos.0, BuildKind::Deconstruct);
267 }
268 }
269 }
270
271 #[cfg(test)]
272 mod tests {
273 use super::*;
274
275 fn walled(map: &mut WorldMap, lo: UVec2, hi: UVec2) {
276 for y in lo.y..=hi.y {
277 for x in lo.x..=hi.x {
278 if x == lo.x || x == hi.x || y == lo.y || y == hi.y {
279 map.set_wall(UVec2::new(x, y), true);
280 }
281 }
282 }
283 }
284
285 // Rooms come from the map, not from plans: any enclosed space with a door that is big enough
286 // and small enough to roof counts, however and in whatever order it was built, and two rooms
287 // can share a wall.
288 #[test]
289 fn rooms_are_enclosed_spaces_with_a_door() {
290 let mut map = WorldMap::filled(UVec2::splat(30), Terrain::Grass);
291 walled(&mut map, UVec2::new(2, 2), UVec2::new(6, 5));
292 walled(&mut map, UVec2::new(6, 2), UVec2::new(10, 5));
293 walled(&mut map, UVec2::new(15, 15), UVec2::new(18, 18));
294 let doors = [UVec2::new(4, 2), UVec2::new(8, 2)];
295 for door in doors {
296 map.set_wall(door, false);
297 }
298 let rooms = detect_rooms(&map, &doors.into_iter().collect());
299 assert_eq!(rooms.len(), 2);
300 assert!(
301 rooms
302 .iter()
303 .all(|(tiles, doors)| tiles.len() == 6 && doors.len() == 1)
304 );
305 }
306
307 #[test]
308 fn quality_rewards_floors_furniture_and_space() {
309 let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
310 let tiles: Vec<UVec2> = (1..5)
311 .flat_map(|y| (1..5).map(move |x| UVec2::new(x, y)))
312 .collect();
313 let bare = room_quality(&map, &tiles, Furnishing::default());
314 for &t in &tiles {
315 map.set(t, Terrain::WoodFloor);
316 map.set_roof(t, true);
317 }
318 let furnished = Furnishing {
319 beds: 2,
320 statue: true,
321 hearth: true,
322 };
323 let nice = room_quality(&map, &tiles, furnished);
324 assert!(nice > bare + 0.5, "{bare} {nice}");
325 let packed = Furnishing {
326 beds: 6,
327 ..furnished
328 };
329 assert!(room_quality(&map, &tiles, packed) < nice);
330 }
331
332 // A door needs walls on two opposite sides and open ground on the others to be passable; one
333 // boxed in by walls on three sides leads nowhere and gets taken down.
334 #[test]
335 fn doors_walled_in_on_three_sides_are_bad() {
336 let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
337 map.set_wall(UVec2::new(4, 5), true);
338 map.set_wall(UVec2::new(6, 5), true);
339 assert!(!bad_door(&map, UVec2::new(5, 5)));
340 map.set_wall(UVec2::new(5, 6), true);
341 assert!(bad_door(&map, UVec2::new(5, 5)));
342 }
343
344 // A room built by hand, wall by wall, gets a roof once it has a bed in it; a door boxed in
345 // on three sides gets taken down.
346 #[test]
347 fn lived_in_rooms_are_roofed_and_bad_doors_removed() {
348 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
349 walled(&mut map, UVec2::new(2, 2), UVec2::new(6, 5));
350 let door = UVec2::new(4, 2);
351 map.set_wall(door, false);
352 let boxed = UVec2::new(12, 12);
353 for side in [IVec2::X, IVec2::NEG_X, IVec2::Y] {
354 map.set_wall((boxed.as_ivec2() + side).as_uvec2(), true);
355 }
356 let mut app = App::new();
357 app.insert_resource(map)
358 .init_resource::<SimTick>()
359 .init_resource::<Rooms>()
360 .init_resource::<Blueprints>()
361 .add_systems(Update, (update_rooms, roof_rooms, remove_bad_doors).chain());
362 let structure = |kind| Structure { kind };
363 let world = app.world_mut();
364 world.spawn((structure(StructureKind::Door), TilePos(door)));
365 world.spawn((structure(StructureKind::Door), TilePos(boxed)));
366 world.spawn((structure(StructureKind::Bed), TilePos(UVec2::new(3, 3))));
367 app.update();
368 let blueprints = app.world().resource::<Blueprints>();
369 let roofs = blueprints
370 .0
371 .values()
372 .filter(|k| **k == BuildKind::Roof)
373 .count();
374 assert_eq!(roofs, 6);
375 assert_eq!(blueprints.get(boxed), Some(BuildKind::Deconstruct));
376 assert_eq!(blueprints.get(door), None);
377 let rooms = app.world().resource::<Rooms>();
378 assert!(rooms.at(UVec2::new(3, 3)).is_some_and(|r| r.quality > 0.0));
379 }
380 }