| 1 |
use std::collections::{HashMap, HashSet, VecDeque}; |
| 2 |
|
| 3 |
use bevy::prelude::*; |
| 4 |
|
| 5 |
use crate::{ |
| 6 |
colonist::Colonist, |
| 7 |
items::{Item, ItemKind}, |
| 8 |
sim::{SimSystems, SimTick}, |
| 9 |
structures::{Structure, StructureKind}, |
| 10 |
world::{TILE_SIZE, TilePos, WorldMap}, |
| 11 |
worldgen::WorldGenerated, |
| 12 |
zones::{Zones, can_zone}, |
| 13 |
}; |
| 14 |
|
| 15 |
pub struct BuildingPlugin; |
| 16 |
|
| 17 |
impl Plugin for BuildingPlugin { |
| 18 |
fn build(&self, app: &mut App) { |
| 19 |
app.init_resource::<Blueprints>() |
| 20 |
.add_systems( |
| 21 |
Update, |
| 22 |
( |
| 23 |
clear_blueprints.run_if(on_message::<WorldGenerated>), |
| 24 |
draw_blueprints, |
| 25 |
), |
| 26 |
) |
| 27 |
.add_systems(FixedUpdate, plan_shelter.in_set(SimSystems::Decide)); |
| 28 |
} |
| 29 |
} |
| 30 |
|
| 31 |
pub const MAX_ROOM_TILES: usize = 120; |
| 32 |
const PLAN_INTERVAL_TICKS: u64 = 300; |
| 33 |
const MAX_ROOM_MARGIN: i32 = 2; |
| 34 |
const CRAMPED_SIDE_COST: i32 = 3; |
| 35 |
const WALL_BLUEPRINT_COLOR: Color = Color::srgba(0.5, 0.75, 1.0, 0.8); |
| 36 |
const DOOR_BLUEPRINT_COLOR: Color = Color::srgba(1.0, 0.8, 0.4, 0.8); |
| 37 |
const ROOF_BLUEPRINT_COLOR: Color = Color::srgba(0.8, 0.6, 1.0, 0.35); |
| 38 |
|
| 39 |
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| 40 |
pub enum BuildKind { |
| 41 |
Structure(StructureKind), |
| 42 |
Roof, |
| 43 |
} |
| 44 |
|
| 45 |
impl BuildKind { |
| 46 |
pub fn cost(self) -> (ItemKind, u32) { |
| 47 |
match self { |
| 48 |
BuildKind::Structure(kind) => kind.cost(), |
| 49 |
BuildKind::Roof => (ItemKind::Wood, 1), |
| 50 |
} |
| 51 |
} |
| 52 |
|
| 53 |
pub fn work(self) -> u32 { |
| 54 |
match self { |
| 55 |
BuildKind::Structure(StructureKind::Campfire) => 360, |
| 56 |
BuildKind::Structure(StructureKind::Torch) => 120, |
| 57 |
BuildKind::Structure(StructureKind::Bed) => 600, |
| 58 |
BuildKind::Structure(StructureKind::Wall) => 300, |
| 59 |
BuildKind::Structure(StructureKind::Door) => 300, |
| 60 |
BuildKind::Roof => 120, |
| 61 |
} |
| 62 |
} |
| 63 |
|
| 64 |
pub fn name(self) -> &'static str { |
| 65 |
match self { |
| 66 |
BuildKind::Structure(kind) => kind.name(), |
| 67 |
BuildKind::Roof => "roof", |
| 68 |
} |
| 69 |
} |
| 70 |
|
| 71 |
pub fn blocks_tile(self) -> bool { |
| 72 |
self == BuildKind::Structure(StructureKind::Wall) |
| 73 |
} |
| 74 |
} |
| 75 |
|
| 76 |
#[derive(Resource, Default, Debug, Clone)] |
| 77 |
pub struct Blueprints(pub HashMap<UVec2, BuildKind>); |
| 78 |
|
| 79 |
impl Blueprints { |
| 80 |
pub fn get(&self, tile: UVec2) -> Option<BuildKind> { |
| 81 |
self.0.get(&tile).copied() |
| 82 |
} |
| 83 |
|
| 84 |
pub fn tiles(&self) -> HashSet<UVec2> { |
| 85 |
self.0.keys().copied().collect() |
| 86 |
} |
| 87 |
} |
| 88 |
|
| 89 |
fn clear_blueprints(mut blueprints: ResMut<Blueprints>) { |
| 90 |
blueprints.0.clear(); |
| 91 |
} |
| 92 |
|
| 93 |
pub fn enclosed_room(map: &WorldMap, doors: &HashSet<UVec2>, start: UVec2) -> Option<Vec<UVec2>> { |
| 94 |
let mut seen = HashSet::from([start]); |
| 95 |
let mut queue = VecDeque::from([start]); |
| 96 |
while let Some(tile) = queue.pop_front() { |
| 97 |
if seen.len() > MAX_ROOM_TILES { |
| 98 |
return None; |
| 99 |
} |
| 100 |
for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] { |
| 101 |
let next = tile.as_ivec2() + step; |
| 102 |
if !map.in_bounds(next) { |
| 103 |
return None; |
| 104 |
} |
| 105 |
let next = next.as_uvec2(); |
| 106 |
let barrier = map.is_wall(next.as_ivec2()) |
| 107 |
|| doors.contains(&next) |
| 108 |
|| map.get(next.as_ivec2()).is_some_and(|t| t.is_rock()); |
| 109 |
if !barrier && seen.insert(next) { |
| 110 |
queue.push_back(next); |
| 111 |
} |
| 112 |
} |
| 113 |
} |
| 114 |
let mut room: Vec<UVec2> = seen.into_iter().collect(); |
| 115 |
room.sort_by_key(|t| (t.y, t.x)); |
| 116 |
Some(room) |
| 117 |
} |
| 118 |
|
| 119 |
pub struct ShelterContext<'a> { |
| 120 |
pub map: &'a WorldMap, |
| 121 |
pub structures: &'a [(UVec2, StructureKind)], |
| 122 |
pub items: &'a [UVec2], |
| 123 |
} |
| 124 |
|
| 125 |
impl ShelterContext<'_> { |
| 126 |
fn doors(&self) -> HashSet<UVec2> { |
| 127 |
self.structures |
| 128 |
.iter() |
| 129 |
.filter(|(_, kind)| *kind == StructureKind::Door) |
| 130 |
.map(|(pos, _)| *pos) |
| 131 |
.collect() |
| 132 |
} |
| 133 |
|
| 134 |
fn wall_buildable(&self, tile: IVec2) -> bool { |
| 135 |
can_zone(self.map, tile) |
| 136 |
&& self.map.plant(tile).is_none() |
| 137 |
&& !self.items.contains(&tile.as_uvec2()) |
| 138 |
&& !self |
| 139 |
.structures |
| 140 |
.iter() |
| 141 |
.any(|(pos, _)| *pos == tile.as_uvec2()) |
| 142 |
} |
| 143 |
|
| 144 |
fn already_closed(&self, tile: IVec2, doors: &HashSet<UVec2>) -> bool { |
| 145 |
self.map.is_wall(tile) |
| 146 |
|| doors.contains(&tile.as_uvec2()) |
| 147 |
|| self.map.get(tile).is_some_and(|t| t.is_rock()) |
| 148 |
} |
| 149 |
|
| 150 |
pub fn plan(&self, beds: &[UVec2], toward: UVec2) -> Option<HashMap<UVec2, BuildKind>> { |
| 151 |
let doors = self.doors(); |
| 152 |
if let Some(&first) = beds.first() |
| 153 |
&& let Some(room) = enclosed_room(self.map, &doors, first) |
| 154 |
{ |
| 155 |
let roofs: HashMap<UVec2, BuildKind> = room |
| 156 |
.into_iter() |
| 157 |
.filter(|t| !self.map.is_roofed(t.as_ivec2())) |
| 158 |
.map(|t| (t, BuildKind::Roof)) |
| 159 |
.collect(); |
| 160 |
return (!roofs.is_empty()).then_some(roofs); |
| 161 |
} |
| 162 |
let min = beds.iter().copied().reduce(|a, b| a.min(b))?.as_ivec2(); |
| 163 |
let max = beds.iter().copied().reduce(|a, b| a.max(b))?.as_ivec2(); |
| 164 |
let margins = 0..=MAX_ROOM_MARGIN; |
| 165 |
let shapes = margins.clone().flat_map(|l| { |
| 166 |
let margins = margins.clone(); |
| 167 |
margins.clone().flat_map(move |r| { |
| 168 |
let margins = margins.clone(); |
| 169 |
margins |
| 170 |
.clone() |
| 171 |
.flat_map(move |d| margins.clone().map(move |u| IVec4::new(l, r, d, u))) |
| 172 |
}) |
| 173 |
}); |
| 174 |
shapes |
| 175 |
.filter_map(|m| { |
| 176 |
let lo = min - IVec2::new(m.x, m.z) - 1; |
| 177 |
let hi = max + IVec2::new(m.y, m.w) + 1; |
| 178 |
let plan = self.walls_around(lo, hi, toward, &doors)?; |
| 179 |
let cramped = m.to_array().iter().filter(|&&side| side == 0).count() as i32; |
| 180 |
let cost = plan.len() as i32 + CRAMPED_SIDE_COST * cramped; |
| 181 |
Some((cost, m.to_array(), plan)) |
| 182 |
}) |
| 183 |
.min_by_key(|(cost, margins, _)| (*cost, *margins)) |
| 184 |
.map(|(_, _, plan)| plan) |
| 185 |
} |
| 186 |
|
| 187 |
fn walls_around( |
| 188 |
&self, |
| 189 |
lo: IVec2, |
| 190 |
hi: IVec2, |
| 191 |
toward: UVec2, |
| 192 |
doors: &HashSet<UVec2>, |
| 193 |
) -> Option<HashMap<UVec2, BuildKind>> { |
| 194 |
let ring: Vec<IVec2> = (lo.y..=hi.y) |
| 195 |
.flat_map(|y| (lo.x..=hi.x).map(move |x| IVec2::new(x, y))) |
| 196 |
.filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y) |
| 197 |
.collect(); |
| 198 |
let mut plan = HashMap::new(); |
| 199 |
for &tile in &ring { |
| 200 |
if !self.map.in_bounds(tile) { |
| 201 |
return None; |
| 202 |
} |
| 203 |
if self.already_closed(tile, doors) { |
| 204 |
continue; |
| 205 |
} |
| 206 |
if !self.wall_buildable(tile) { |
| 207 |
return None; |
| 208 |
} |
| 209 |
plan.insert(tile.as_uvec2(), BuildKind::Structure(StructureKind::Wall)); |
| 210 |
} |
| 211 |
let has_door = ring.iter().any(|t| doors.contains(&t.as_uvec2())); |
| 212 |
if !has_door { |
| 213 |
let corner = |t: &UVec2| { |
| 214 |
let t = t.as_ivec2(); |
| 215 |
(t.x == lo.x || t.x == hi.x) && (t.y == lo.y || t.y == hi.y) |
| 216 |
}; |
| 217 |
let door = plan.keys().filter(|t| !corner(t)).copied().min_by(|a, b| { |
| 218 |
a.as_vec2() |
| 219 |
.distance_squared(toward.as_vec2()) |
| 220 |
.total_cmp(&b.as_vec2().distance_squared(toward.as_vec2())) |
| 221 |
.then((a.y, a.x).cmp(&(b.y, b.x))) |
| 222 |
})?; |
| 223 |
plan.insert(door, BuildKind::Structure(StructureKind::Door)); |
| 224 |
} |
| 225 |
Some(plan) |
| 226 |
} |
| 227 |
} |
| 228 |
|
| 229 |
pub fn plan_shelter( |
| 230 |
tick: Res<SimTick>, |
| 231 |
map: Res<WorldMap>, |
| 232 |
mut zones: ResMut<Zones>, |
| 233 |
structures: Query<(&TilePos, &Structure), Without<Colonist>>, |
| 234 |
items: Query<&TilePos, With<Item>>, |
| 235 |
mut blueprints: ResMut<Blueprints>, |
| 236 |
) { |
| 237 |
if !tick.0.is_multiple_of(PLAN_INTERVAL_TICKS) || !blueprints.0.is_empty() { |
| 238 |
return; |
| 239 |
} |
| 240 |
let structures: Vec<(UVec2, StructureKind)> = |
| 241 |
structures.iter().map(|(pos, s)| (pos.0, s.kind)).collect(); |
| 242 |
let beds: Vec<UVec2> = structures |
| 243 |
.iter() |
| 244 |
.filter(|(pos, kind)| *kind == StructureKind::Bed && !map.is_roofed(pos.as_ivec2())) |
| 245 |
.map(|(pos, _)| *pos) |
| 246 |
.collect(); |
| 247 |
if beds.is_empty() { |
| 248 |
return; |
| 249 |
} |
| 250 |
let items: Vec<UVec2> = items.iter().map(|pos| pos.0).collect(); |
| 251 |
let ctx = ShelterContext { |
| 252 |
map: &map, |
| 253 |
structures: &structures, |
| 254 |
items: &items, |
| 255 |
}; |
| 256 |
let toward = structures |
| 257 |
.iter() |
| 258 |
.find(|(_, kind)| *kind == StructureKind::Campfire) |
| 259 |
.map(|(pos, _)| *pos) |
| 260 |
.unwrap_or_else(|| zones.home(beds[0])); |
| 261 |
if let Some(plan) = ctx.plan(&beds, toward) { |
| 262 |
for (tile, kind) in &plan { |
| 263 |
if *kind != BuildKind::Roof { |
| 264 |
zones.remove_tile(*tile); |
| 265 |
} |
| 266 |
} |
| 267 |
blueprints.0 = plan; |
| 268 |
} |
| 269 |
} |
| 270 |
|
| 271 |
fn draw_blueprints(mut gizmos: Gizmos, blueprints: Res<Blueprints>) { |
| 272 |
for (tile, kind) in &blueprints.0 { |
| 273 |
let color = match kind { |
| 274 |
BuildKind::Structure(StructureKind::Door) => DOOR_BLUEPRINT_COLOR, |
| 275 |
BuildKind::Roof => ROOF_BLUEPRINT_COLOR, |
| 276 |
_ => WALL_BLUEPRINT_COLOR, |
| 277 |
}; |
| 278 |
gizmos.rect_2d( |
| 279 |
WorldMap::tile_center(*tile), |
| 280 |
Vec2::splat(TILE_SIZE * 0.85), |
| 281 |
color, |
| 282 |
); |
| 283 |
} |
| 284 |
} |
| 285 |
|
| 286 |
#[cfg(test)] |
| 287 |
mod tests { |
| 288 |
use super::*; |
| 289 |
use crate::world::Terrain; |
| 290 |
|
| 291 |
fn ctx<'a>(map: &'a WorldMap, structures: &'a [(UVec2, StructureKind)]) -> ShelterContext<'a> { |
| 292 |
ShelterContext { |
| 293 |
map, |
| 294 |
structures, |
| 295 |
items: &[], |
| 296 |
} |
| 297 |
} |
| 298 |
|
| 299 |
fn count(plan: &HashMap<UVec2, BuildKind>, kind: BuildKind) -> usize { |
| 300 |
plan.values().filter(|k| **k == kind).count() |
| 301 |
} |
| 302 |
|
| 303 |
const WALL: BuildKind = BuildKind::Structure(StructureKind::Wall); |
| 304 |
const DOOR: BuildKind = BuildKind::Structure(StructureKind::Door); |
| 305 |
|
| 306 |
#[test] |
| 307 |
fn shelter_is_a_wall_ring_with_one_door_around_the_beds() { |
| 308 |
let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 309 |
let beds = [UVec2::new(10, 10), UVec2::new(11, 10)]; |
| 310 |
let plan = ctx(&map, &[]).plan(&beds, UVec2::new(10, 2)).unwrap(); |
| 311 |
assert_eq!(plan.len(), 6 * 5 - 4 * 3); |
| 312 |
assert_eq!(count(&plan, DOOR), 1); |
| 313 |
let door = plan.iter().find(|(_, k)| **k == DOOR).unwrap().0; |
| 314 |
assert_eq!(door.y, 8); |
| 315 |
} |
| 316 |
|
| 317 |
|
| 318 |
#[test] |
| 319 |
fn rock_counts_as_wall() { |
| 320 |
let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 321 |
for y in 0..20 { |
| 322 |
map.set(UVec2::new(12, y), Terrain::Stone); |
| 323 |
} |
| 324 |
let plan = ctx(&map, &[]) |
| 325 |
.plan(&[UVec2::new(10, 10)], UVec2::new(10, 2)) |
| 326 |
.unwrap(); |
| 327 |
assert!(plan.keys().all(|t| t.x != 12)); |
| 328 |
assert_eq!(count(&plan, WALL) + count(&plan, DOOR), 16 - 5); |
| 329 |
} |
| 330 |
|
| 331 |
#[test] |
| 332 |
fn shelters_route_around_unbuildable_tiles() { |
| 333 |
let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 334 |
map.set(UVec2::new(10, 8), Terrain::ShallowWater); |
| 335 |
let plan = ctx(&map, &[]) |
| 336 |
.plan(&[UVec2::new(10, 10)], UVec2::new(10, 2)) |
| 337 |
.unwrap(); |
| 338 |
assert!(!plan.contains_key(&UVec2::new(10, 8))); |
| 339 |
assert_eq!( |
| 340 |
plan.values() |
| 341 |
.filter(|k| **k == BuildKind::Structure(StructureKind::Door)) |
| 342 |
.count(), |
| 343 |
1 |
| 344 |
); |
| 345 |
} |
| 346 |
|
| 347 |
|
| 348 |
|
| 349 |
#[test] |
| 350 |
fn walls_may_replace_empty_stockpile_tiles_but_not_stored_items() { |
| 351 |
let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 352 |
let mut zones = Zones::default(); |
| 353 |
zones.add_stockpile(&map, IVec2::new(12, 5), IVec2::new(15, 15)); |
| 354 |
let items = [UVec2::new(13, 10)]; |
| 355 |
let ctx = ShelterContext { |
| 356 |
map: &map, |
| 357 |
structures: &[], |
| 358 |
items: &items, |
| 359 |
}; |
| 360 |
let plan = ctx.plan(&[UVec2::new(11, 10)], UVec2::new(5, 10)).unwrap(); |
| 361 |
assert!(plan.keys().any(|t| zones.is_stockpile(*t))); |
| 362 |
assert!(!plan.contains_key(&UVec2::new(13, 10))); |
| 363 |
} |
| 364 |
|
| 365 |
#[test] |
| 366 |
fn enclosed_rooms_get_roof_blueprints() { |
| 367 |
let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass); |
| 368 |
for i in 3..=6 { |
| 369 |
for t in [ |
| 370 |
UVec2::new(i, 3), |
| 371 |
UVec2::new(i, 6), |
| 372 |
UVec2::new(3, i), |
| 373 |
UVec2::new(6, i), |
| 374 |
] { |
| 375 |
map.set_wall(t, true); |
| 376 |
} |
| 377 |
} |
| 378 |
map.set_wall(UVec2::new(4, 3), false); |
| 379 |
let structures = [(UVec2::new(4, 3), StructureKind::Door)]; |
| 380 |
let plan = ctx(&map, &structures) |
| 381 |
.plan(&[UVec2::new(4, 4)], UVec2::ZERO) |
| 382 |
.unwrap(); |
| 383 |
assert_eq!(plan.len(), 4); |
| 384 |
assert!(plan.values().all(|k| *k == BuildKind::Roof)); |
| 385 |
} |
| 386 |
|
| 387 |
#[test] |
| 388 |
fn open_areas_are_not_rooms() { |
| 389 |
let map = WorldMap::filled(UVec2::splat(30), Terrain::Grass); |
| 390 |
assert!(enclosed_room(&map, &HashSet::new(), UVec2::new(15, 15)).is_none()); |
| 391 |
} |
| 392 |
} |