| 1 |
use serde::{Deserialize, Serialize}; |
| 2 |
use std::collections::{HashMap, HashSet, VecDeque}; |
| 3 |
|
| 4 |
use bevy::prelude::*; |
| 5 |
|
| 6 |
use crate::{ |
| 7 |
actions::Action, |
| 8 |
colonist::Colonist, |
| 9 |
family::Homes, |
| 10 |
items::{Item, ItemKind}, |
| 11 |
paths::busy_path, |
| 12 |
rooms::Rooms, |
| 13 |
selection::{Cursor, update_cursor}, |
| 14 |
sim::{SimSystems, SimTick}, |
| 15 |
structures::{Structure, StructureKind}, |
| 16 |
ui::PointerOverUi, |
| 17 |
world::{TILE_SIZE, Terrain, TilePos, WorldMap}, |
| 18 |
worldgen::WorldGenerated, |
| 19 |
zones::{PREVIEW_ADD_COLOR, PREVIEW_REMOVE_COLOR, Zones, can_zone, removing}, |
| 20 |
}; |
| 21 |
|
| 22 |
pub struct BuildingPlugin; |
| 23 |
|
| 24 |
impl Plugin for BuildingPlugin { |
| 25 |
fn build(&self, app: &mut App) { |
| 26 |
app.init_resource::<Blueprints>() |
| 27 |
.add_systems( |
| 28 |
Update, |
| 29 |
( |
| 30 |
clear_blueprints.run_if(on_message::<WorldGenerated>), |
| 31 |
draw_blueprints, |
| 32 |
), |
| 33 |
) |
| 34 |
.init_resource::<BuildTool>() |
| 35 |
.add_systems( |
| 36 |
PostUpdate, |
| 37 |
(build_tool, draw_build_preview) |
| 38 |
.chain() |
| 39 |
.after(update_cursor), |
| 40 |
) |
| 41 |
.add_systems(FixedUpdate, plan_shelter.in_set(SimSystems::Decide)); |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
pub const MAX_ROOM_TILES: usize = 120; |
| 46 |
const PLAN_INTERVAL_TICKS: u64 = 300; |
| 47 |
const MAX_ROOM_MARGIN: i32 = 2; |
| 48 |
const CRAMPED_SIDE_COST: i32 = 3; |
| 49 |
const WALL_BLUEPRINT_COLOR: Color = Color::srgba(0.5, 0.75, 1.0, 0.8); |
| 50 |
const DOOR_BLUEPRINT_COLOR: Color = Color::srgba(1.0, 0.8, 0.4, 0.8); |
| 51 |
const PAVE_BLUEPRINT_COLOR: Color = Color::srgba(0.85, 0.85, 0.85, 0.5); |
| 52 |
const DECONSTRUCT_COLOR: Color = Color::srgba(1.0, 0.3, 0.25, 0.9); |
| 53 |
const FLOOR_BLUEPRINT_COLOR: Color = Color::srgba(0.75, 0.5, 0.25, 0.6); |
| 54 |
const ROOF_BLUEPRINT_COLOR: Color = Color::srgba(0.8, 0.6, 1.0, 0.35); |
| 55 |
|
| 56 |
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 57 |
pub enum BuildKind { |
| 58 |
Structure(StructureKind), |
| 59 |
Roof, |
| 60 |
Pave, |
| 61 |
Floor, |
| 62 |
Refuel(StructureKind), |
| 63 |
Deconstruct, |
| 64 |
} |
| 65 |
|
| 66 |
impl BuildKind { |
| 67 |
pub fn cost(self) -> (ItemKind, u32) { |
| 68 |
match self { |
| 69 |
BuildKind::Structure(kind) => kind.cost(), |
| 70 |
BuildKind::Roof => (ItemKind::Wood, 1), |
| 71 |
BuildKind::Pave => (ItemKind::Stone, 1), |
| 72 |
BuildKind::Floor => (ItemKind::Wood, 1), |
| 73 |
BuildKind::Refuel(kind) => kind.refuel_cost(), |
| 74 |
BuildKind::Deconstruct => (ItemKind::Wood, 0), |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
pub fn work(self) -> u32 { |
| 79 |
match self { |
| 80 |
BuildKind::Structure(StructureKind::Campfire) => 360, |
| 81 |
BuildKind::Structure(StructureKind::Torch) => 120, |
| 82 |
BuildKind::Structure(StructureKind::Bed) => 600, |
| 83 |
BuildKind::Structure(StructureKind::Wall) => 300, |
| 84 |
BuildKind::Structure(StructureKind::Door) => 300, |
| 85 |
BuildKind::Structure(StructureKind::Workbench) => 600, |
| 86 |
BuildKind::Structure(StructureKind::Statue) => 900, |
| 87 |
BuildKind::Structure(StructureKind::Hearth) => 400, |
| 88 |
BuildKind::Roof => 120, |
| 89 |
BuildKind::Pave => 120, |
| 90 |
BuildKind::Floor => 90, |
| 91 |
BuildKind::Refuel(_) => 60, |
| 92 |
BuildKind::Deconstruct => 200, |
| 93 |
} |
| 94 |
} |
| 95 |
|
| 96 |
pub fn name(self) -> &'static str { |
| 97 |
match self { |
| 98 |
BuildKind::Structure(kind) => kind.name(), |
| 99 |
BuildKind::Roof => "roof", |
| 100 |
BuildKind::Pave => "paving", |
| 101 |
BuildKind::Floor => "floor", |
| 102 |
BuildKind::Refuel(_) => "refuelling", |
| 103 |
BuildKind::Deconstruct => "deconstruction", |
| 104 |
} |
| 105 |
} |
| 106 |
|
| 107 |
pub fn blocks_tile(self) -> bool { |
| 108 |
self == BuildKind::Structure(StructureKind::Wall) |
| 109 |
} |
| 110 |
} |
| 111 |
|
| 112 |
#[derive(Resource, Default, Debug, Clone)] |
| 113 |
pub struct Blueprints(pub HashMap<UVec2, BuildKind>); |
| 114 |
|
| 115 |
impl Blueprints { |
| 116 |
pub fn get(&self, tile: UVec2) -> Option<BuildKind> { |
| 117 |
self.0.get(&tile).copied() |
| 118 |
} |
| 119 |
|
| 120 |
pub fn tiles(&self) -> HashSet<UVec2> { |
| 121 |
self.0.keys().copied().collect() |
| 122 |
} |
| 123 |
} |
| 124 |
|
| 125 |
fn clear_blueprints(mut blueprints: ResMut<Blueprints>) { |
| 126 |
blueprints.0.clear(); |
| 127 |
} |
| 128 |
|
| 129 |
pub fn enclosed_room(map: &WorldMap, doors: &HashSet<UVec2>, start: UVec2) -> Option<Vec<UVec2>> { |
| 130 |
let mut seen = HashSet::from([start]); |
| 131 |
let mut queue = VecDeque::from([start]); |
| 132 |
while let Some(tile) = queue.pop_front() { |
| 133 |
if seen.len() > MAX_ROOM_TILES { |
| 134 |
return None; |
| 135 |
} |
| 136 |
for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] { |
| 137 |
let next = tile.as_ivec2() + step; |
| 138 |
if !map.in_bounds(next) { |
| 139 |
return None; |
| 140 |
} |
| 141 |
let next = next.as_uvec2(); |
| 142 |
let barrier = map.is_wall(next.as_ivec2()) |
| 143 |
|| doors.contains(&next) |
| 144 |
|| map.get(next.as_ivec2()).is_some_and(|t| t.is_rock()); |
| 145 |
if !barrier && seen.insert(next) { |
| 146 |
queue.push_back(next); |
| 147 |
} |
| 148 |
} |
| 149 |
} |
| 150 |
let mut room: Vec<UVec2> = seen.into_iter().collect(); |
| 151 |
room.sort_by_key(|t| (t.y, t.x)); |
| 152 |
Some(room) |
| 153 |
} |
| 154 |
|
| 155 |
pub struct ShelterContext<'a> { |
| 156 |
pub map: &'a WorldMap, |
| 157 |
pub structures: &'a [(UVec2, StructureKind)], |
| 158 |
pub items: &'a [UVec2], |
| 159 |
} |
| 160 |
|
| 161 |
impl ShelterContext<'_> { |
| 162 |
fn doors(&self) -> HashSet<UVec2> { |
| 163 |
self.structures |
| 164 |
.iter() |
| 165 |
.filter(|(_, kind)| *kind == StructureKind::Door) |
| 166 |
.map(|(pos, _)| *pos) |
| 167 |
.collect() |
| 168 |
} |
| 169 |
|
| 170 |
fn wall_buildable(&self, tile: IVec2) -> bool { |
| 171 |
can_zone(self.map, tile) |
| 172 |
&& !busy_path(self.map, tile) |
| 173 |
&& self.map.plant(tile).is_none() |
| 174 |
&& !self.items.contains(&tile.as_uvec2()) |
| 175 |
&& !self |
| 176 |
.structures |
| 177 |
.iter() |
| 178 |
.any(|(pos, _)| *pos == tile.as_uvec2()) |
| 179 |
} |
| 180 |
|
| 181 |
fn already_closed(&self, tile: IVec2, doors: &HashSet<UVec2>) -> bool { |
| 182 |
self.map.is_wall(tile) |
| 183 |
|| doors.contains(&tile.as_uvec2()) |
| 184 |
|| self.map.get(tile).is_some_and(|t| t.is_rock()) |
| 185 |
} |
| 186 |
|
| 187 |
pub fn plan(&self, beds: &[UVec2], toward: UVec2) -> Option<HashMap<UVec2, BuildKind>> { |
| 188 |
let doors = self.doors(); |
| 189 |
if let Some(&first) = beds.first() |
| 190 |
&& let Some(room) = enclosed_room(self.map, &doors, first) |
| 191 |
{ |
| 192 |
let roofs: HashMap<UVec2, BuildKind> = room |
| 193 |
.into_iter() |
| 194 |
.filter(|t| !self.map.is_roofed(t.as_ivec2())) |
| 195 |
.map(|t| (t, BuildKind::Roof)) |
| 196 |
.collect(); |
| 197 |
return (!roofs.is_empty()).then_some(roofs); |
| 198 |
} |
| 199 |
let min = beds.iter().copied().reduce(|a, b| a.min(b))?.as_ivec2(); |
| 200 |
let max = beds.iter().copied().reduce(|a, b| a.max(b))?.as_ivec2(); |
| 201 |
let margins = 0..=MAX_ROOM_MARGIN; |
| 202 |
let shapes = margins.clone().flat_map(|l| { |
| 203 |
let margins = margins.clone(); |
| 204 |
margins.clone().flat_map(move |r| { |
| 205 |
let margins = margins.clone(); |
| 206 |
margins |
| 207 |
.clone() |
| 208 |
.flat_map(move |d| margins.clone().map(move |u| IVec4::new(l, r, d, u))) |
| 209 |
}) |
| 210 |
}); |
| 211 |
shapes |
| 212 |
.filter_map(|m| { |
| 213 |
let lo = min - IVec2::new(m.x, m.z) - 1; |
| 214 |
let hi = max + IVec2::new(m.y, m.w) + 1; |
| 215 |
let plan = self.walls_around(lo, hi, toward, &doors)?; |
| 216 |
let cramped = m.to_array().iter().filter(|&&side| side == 0).count() as i32; |
| 217 |
let cost = plan.len() as i32 + CRAMPED_SIDE_COST * cramped; |
| 218 |
Some((cost, m.to_array(), plan)) |
| 219 |
}) |
| 220 |
.min_by_key(|(cost, margins, _)| (*cost, *margins)) |
| 221 |
.map(|(_, _, plan)| plan) |
| 222 |
} |
| 223 |
|
| 224 |
fn walls_around( |
| 225 |
&self, |
| 226 |
lo: IVec2, |
| 227 |
hi: IVec2, |
| 228 |
toward: UVec2, |
| 229 |
doors: &HashSet<UVec2>, |
| 230 |
) -> Option<HashMap<UVec2, BuildKind>> { |
| 231 |
let ring: Vec<IVec2> = (lo.y..=hi.y) |
| 232 |
.flat_map(|y| (lo.x..=hi.x).map(move |x| IVec2::new(x, y))) |
| 233 |
.filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y) |
| 234 |
.collect(); |
| 235 |
let mut plan = HashMap::new(); |
| 236 |
for &tile in &ring { |
| 237 |
if !self.map.in_bounds(tile) { |
| 238 |
return None; |
| 239 |
} |
| 240 |
if self.already_closed(tile, doors) { |
| 241 |
continue; |
| 242 |
} |
| 243 |
if !self.wall_buildable(tile) { |
| 244 |
return None; |
| 245 |
} |
| 246 |
plan.insert(tile.as_uvec2(), BuildKind::Structure(StructureKind::Wall)); |
| 247 |
} |
| 248 |
let has_door = ring.iter().any(|t| doors.contains(&t.as_uvec2())); |
| 249 |
if !has_door { |
| 250 |
let corner = |t: &UVec2| { |
| 251 |
let t = t.as_ivec2(); |
| 252 |
(t.x == lo.x || t.x == hi.x) && (t.y == lo.y || t.y == hi.y) |
| 253 |
}; |
| 254 |
let door = plan.keys().filter(|t| !corner(t)).copied().min_by(|a, b| { |
| 255 |
a.as_vec2() |
| 256 |
.distance_squared(toward.as_vec2()) |
| 257 |
.total_cmp(&b.as_vec2().distance_squared(toward.as_vec2())) |
| 258 |
.then((a.y, a.x).cmp(&(b.y, b.x))) |
| 259 |
})?; |
| 260 |
plan.insert(door, BuildKind::Structure(StructureKind::Door)); |
| 261 |
} |
| 262 |
Some(plan) |
| 263 |
} |
| 264 |
} |
| 265 |
|
| 266 |
pub fn plan_shelter( |
| 267 |
tick: Res<SimTick>, |
| 268 |
map: Res<WorldMap>, |
| 269 |
mut zones: ResMut<Zones>, |
| 270 |
structures: Query<(&TilePos, &Structure), Without<Colonist>>, |
| 271 |
items: Query<&TilePos, With<Item>>, |
| 272 |
mut blueprints: ResMut<Blueprints>, |
| 273 |
homes: Res<Homes>, |
| 274 |
rooms: Res<Rooms>, |
| 275 |
) { |
| 276 |
let shelter_pending = blueprints |
| 277 |
.0 |
| 278 |
.values() |
| 279 |
.any(|k| !matches!(k, BuildKind::Pave | BuildKind::Floor)); |
| 280 |
if !tick.0.is_multiple_of(PLAN_INTERVAL_TICKS) || shelter_pending { |
| 281 |
return; |
| 282 |
} |
| 283 |
let structures: Vec<(UVec2, StructureKind)> = |
| 284 |
structures.iter().map(|(pos, s)| (pos.0, s.kind)).collect(); |
| 285 |
let beds: Vec<UVec2> = structures |
| 286 |
.iter() |
| 287 |
.filter(|(pos, kind)| { |
| 288 |
*kind == StructureKind::Bed |
| 289 |
&& !map.is_roofed(pos.as_ivec2()) |
| 290 |
&& !homes.is_home(*pos) |
| 291 |
&& !rooms.contains(*pos) |
| 292 |
}) |
| 293 |
.map(|(pos, _)| *pos) |
| 294 |
.collect(); |
| 295 |
if beds.is_empty() { |
| 296 |
return; |
| 297 |
} |
| 298 |
let items: Vec<UVec2> = items.iter().map(|pos| pos.0).collect(); |
| 299 |
let ctx = ShelterContext { |
| 300 |
map: &map, |
| 301 |
structures: &structures, |
| 302 |
items: &items, |
| 303 |
}; |
| 304 |
let toward = structures |
| 305 |
.iter() |
| 306 |
.find(|(_, kind)| *kind == StructureKind::Campfire) |
| 307 |
.map(|(pos, _)| *pos) |
| 308 |
.unwrap_or_else(|| zones.home(beds[0])); |
| 309 |
if let Some(plan) = ctx.plan(&beds, toward) { |
| 310 |
for (tile, kind) in &plan { |
| 311 |
if *kind != BuildKind::Roof { |
| 312 |
zones.remove_tile(*tile); |
| 313 |
} |
| 314 |
} |
| 315 |
blueprints.0.extend(plan); |
| 316 |
} |
| 317 |
} |
| 318 |
|
| 319 |
#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 320 |
pub enum BuildMode { |
| 321 |
Place(StructureKind), |
| 322 |
Floor, |
| 323 |
Deconstruct, |
| 324 |
} |
| 325 |
|
| 326 |
pub const BUILD_TOOL_CYCLE: [BuildMode; 9] = [ |
| 327 |
BuildMode::Place(StructureKind::Wall), |
| 328 |
BuildMode::Place(StructureKind::Door), |
| 329 |
BuildMode::Place(StructureKind::Campfire), |
| 330 |
BuildMode::Place(StructureKind::Torch), |
| 331 |
BuildMode::Place(StructureKind::Bed), |
| 332 |
BuildMode::Place(StructureKind::Workbench), |
| 333 |
BuildMode::Place(StructureKind::Statue), |
| 334 |
BuildMode::Place(StructureKind::Hearth), |
| 335 |
BuildMode::Floor, |
| 336 |
]; |
| 337 |
|
| 338 |
#[derive(Resource, Default, Debug)] |
| 339 |
pub struct BuildTool { |
| 340 |
pub mode: Option<BuildMode>, |
| 341 |
drag_start: Option<UVec2>, |
| 342 |
} |
| 343 |
|
| 344 |
impl BuildTool { |
| 345 |
pub fn active(&self) -> bool { |
| 346 |
self.mode.is_some() |
| 347 |
} |
| 348 |
|
| 349 |
fn cycle(&mut self) { |
| 350 |
let next = match self.mode { |
| 351 |
None => Some(0), |
| 352 |
Some(mode) => match BUILD_TOOL_CYCLE.iter().position(|m| *m == mode) { |
| 353 |
Some(i) => Some(i + 1).filter(|&i| i < BUILD_TOOL_CYCLE.len()), |
| 354 |
None => Some(0), |
| 355 |
}, |
| 356 |
}; |
| 357 |
self.mode = next.map(|i| BUILD_TOOL_CYCLE[i]); |
| 358 |
} |
| 359 |
} |
| 360 |
|
| 361 |
fn rect(start: UVec2, end: UVec2) -> impl Iterator<Item = UVec2> { |
| 362 |
let (lo, hi) = (start.min(end), start.max(end)); |
| 363 |
(lo.y..=hi.y).flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y))) |
| 364 |
} |
| 365 |
|
| 366 |
pub fn mark_deconstruction( |
| 367 |
map: &WorldMap, |
| 368 |
blueprints: &mut Blueprints, |
| 369 |
occupied: &HashSet<UVec2>, |
| 370 |
start: UVec2, |
| 371 |
end: UVec2, |
| 372 |
) { |
| 373 |
for tile in rect(start, end).filter(|t| occupied.contains(t) || map.is_roofed(t.as_ivec2())) { |
| 374 |
blueprints.0.insert(tile, BuildKind::Deconstruct); |
| 375 |
} |
| 376 |
} |
| 377 |
|
| 378 |
pub fn place_floors(map: &WorldMap, blueprints: &mut Blueprints, start: UVec2, end: UVec2) { |
| 379 |
for tile in rect(start, end) { |
| 380 |
let p = tile.as_ivec2(); |
| 381 |
let floorable = can_zone(map, p) |
| 382 |
&& !map.is_wall(p) |
| 383 |
&& map.plant(p).is_none() |
| 384 |
&& map.get(p) != Some(Terrain::WoodFloor); |
| 385 |
if floorable && blueprints.get(tile).is_none() { |
| 386 |
blueprints.0.insert(tile, BuildKind::Floor); |
| 387 |
} |
| 388 |
} |
| 389 |
} |
| 390 |
|
| 391 |
pub fn placements(kind: StructureKind, start: UVec2, end: UVec2) -> Vec<UVec2> { |
| 392 |
if kind != StructureKind::Wall { |
| 393 |
return vec![end]; |
| 394 |
} |
| 395 |
let (lo, hi) = (start.min(end), start.max(end)); |
| 396 |
(lo.y..=hi.y) |
| 397 |
.flat_map(|y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y))) |
| 398 |
.filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y) |
| 399 |
.collect() |
| 400 |
} |
| 401 |
|
| 402 |
pub fn place_blueprints( |
| 403 |
map: &WorldMap, |
| 404 |
zones: &mut Zones, |
| 405 |
blueprints: &mut Blueprints, |
| 406 |
occupied: &HashSet<UVec2>, |
| 407 |
kind: StructureKind, |
| 408 |
tiles: &[UVec2], |
| 409 |
) { |
| 410 |
for &tile in tiles { |
| 411 |
let free = can_zone(map, tile.as_ivec2()) |
| 412 |
&& map.plant(tile.as_ivec2()).is_none() |
| 413 |
&& !map.is_wall(tile.as_ivec2()) |
| 414 |
&& !occupied.contains(&tile); |
| 415 |
if !free { |
| 416 |
continue; |
| 417 |
} |
| 418 |
if matches!(kind, StructureKind::Wall | StructureKind::Door) { |
| 419 |
zones.remove_tile(tile); |
| 420 |
} |
| 421 |
blueprints.0.insert(tile, BuildKind::Structure(kind)); |
| 422 |
} |
| 423 |
} |
| 424 |
|
| 425 |
#[allow(clippy::too_many_arguments)] |
| 426 |
fn build_tool( |
| 427 |
mut actions: MessageReader<Action>, |
| 428 |
keys: Res<ButtonInput<KeyCode>>, |
| 429 |
buttons: Res<ButtonInput<MouseButton>>, |
| 430 |
over_ui: Res<PointerOverUi>, |
| 431 |
cursor: Res<Cursor>, |
| 432 |
map: Res<WorldMap>, |
| 433 |
mut tool: ResMut<BuildTool>, |
| 434 |
mut zones: ResMut<Zones>, |
| 435 |
mut blueprints: ResMut<Blueprints>, |
| 436 |
structures: Query<&TilePos, (With<Structure>, Without<Colonist>)>, |
| 437 |
) { |
| 438 |
for action in actions.read() { |
| 439 |
match action { |
| 440 |
Action::CycleBuildTool => tool.cycle(), |
| 441 |
Action::ToggleDemolishTool => { |
| 442 |
tool.mode = |
| 443 |
(tool.mode != Some(BuildMode::Deconstruct)).then_some(BuildMode::Deconstruct); |
| 444 |
} |
| 445 |
Action::Cancel |
| 446 |
| Action::ToggleStockpileTool |
| 447 |
| Action::ToggleFarmTool |
| 448 |
| Action::ToggleDesignateTool => tool.mode = None, |
| 449 |
_ => continue, |
| 450 |
} |
| 451 |
tool.drag_start = None; |
| 452 |
return; |
| 453 |
} |
| 454 |
let Some(mode) = tool.mode else { |
| 455 |
return; |
| 456 |
}; |
| 457 |
if buttons.just_pressed(MouseButton::Left) && !over_ui.0 { |
| 458 |
tool.drag_start = cursor.tile; |
| 459 |
} |
| 460 |
if !buttons.just_released(MouseButton::Left) { |
| 461 |
return; |
| 462 |
} |
| 463 |
let (Some(start), Some(end)) = (tool.drag_start.take(), cursor.tile) else { |
| 464 |
return; |
| 465 |
}; |
| 466 |
if removing(&keys) { |
| 467 |
let (lo, hi) = (start.min(end), start.max(end)); |
| 468 |
blueprints |
| 469 |
.0 |
| 470 |
.retain(|t, _| !(t.cmpge(lo).all() && t.cmple(hi).all())); |
| 471 |
return; |
| 472 |
} |
| 473 |
let occupied: HashSet<UVec2> = structures.iter().map(|p| p.0).collect(); |
| 474 |
match mode { |
| 475 |
BuildMode::Place(kind) => { |
| 476 |
let tiles = placements(kind, start, end); |
| 477 |
place_blueprints(&map, &mut zones, &mut blueprints, &occupied, kind, &tiles); |
| 478 |
} |
| 479 |
BuildMode::Floor => place_floors(&map, &mut blueprints, start, end), |
| 480 |
BuildMode::Deconstruct => mark_deconstruction(&map, &mut blueprints, &occupied, start, end), |
| 481 |
} |
| 482 |
} |
| 483 |
|
| 484 |
fn draw_build_preview( |
| 485 |
keys: Res<ButtonInput<KeyCode>>, |
| 486 |
cursor: Res<Cursor>, |
| 487 |
tool: Res<BuildTool>, |
| 488 |
mut gizmos: Gizmos, |
| 489 |
) { |
| 490 |
let (Some(mode), Some(end)) = (tool.mode, cursor.tile) else { |
| 491 |
return; |
| 492 |
}; |
| 493 |
let start = tool.drag_start.unwrap_or(end); |
| 494 |
let color = if removing(&keys) || mode == BuildMode::Deconstruct { |
| 495 |
PREVIEW_REMOVE_COLOR |
| 496 |
} else { |
| 497 |
PREVIEW_ADD_COLOR |
| 498 |
}; |
| 499 |
let tiles: Vec<UVec2> = match mode { |
| 500 |
BuildMode::Place(kind) if !removing(&keys) => placements(kind, start, end), |
| 501 |
_ => rect(start, end).collect(), |
| 502 |
}; |
| 503 |
for tile in tiles { |
| 504 |
gizmos.rect_2d( |
| 505 |
WorldMap::tile_center(tile), |
| 506 |
Vec2::splat(TILE_SIZE * 0.7), |
| 507 |
color, |
| 508 |
); |
| 509 |
} |
| 510 |
} |
| 511 |
|
| 512 |
fn draw_blueprints(mut gizmos: Gizmos, blueprints: Res<Blueprints>) { |
| 513 |
for (tile, kind) in &blueprints.0 { |
| 514 |
let color = match kind { |
| 515 |
BuildKind::Structure(StructureKind::Door) => DOOR_BLUEPRINT_COLOR, |
| 516 |
BuildKind::Roof => ROOF_BLUEPRINT_COLOR, |
| 517 |
BuildKind::Pave => PAVE_BLUEPRINT_COLOR, |
| 518 |
BuildKind::Floor => FLOOR_BLUEPRINT_COLOR, |
| 519 |
BuildKind::Deconstruct => DECONSTRUCT_COLOR, |
| 520 |
_ => WALL_BLUEPRINT_COLOR, |
| 521 |
}; |
| 522 |
gizmos.rect_2d( |
| 523 |
WorldMap::tile_center(*tile), |
| 524 |
Vec2::splat(TILE_SIZE * 0.85), |
| 525 |
color, |
| 526 |
); |
| 527 |
} |
| 528 |
} |
| 529 |
|
| 530 |
#[cfg(test)] |
| 531 |
mod tests { |
| 532 |
use super::*; |
| 533 |
use crate::world::Terrain; |
| 534 |
|
| 535 |
fn ctx<'a>(map: &'a WorldMap, structures: &'a [(UVec2, StructureKind)]) -> ShelterContext<'a> { |
| 536 |
ShelterContext { |
| 537 |
map, |
| 538 |
structures, |
| 539 |
items: &[], |
| 540 |
} |
| 541 |
} |
| 542 |
|
| 543 |
fn count(plan: &HashMap<UVec2, BuildKind>, kind: BuildKind) -> usize { |
| 544 |
plan.values().filter(|k| **k == kind).count() |
| 545 |
} |
| 546 |
|
| 547 |
const WALL: BuildKind = BuildKind::Structure(StructureKind::Wall); |
| 548 |
const DOOR: BuildKind = BuildKind::Structure(StructureKind::Door); |
| 549 |
|
| 550 |
#[test] |
| 551 |
fn the_build_tool_cycles_through_structures_and_off() { |
| 552 |
let mut tool = BuildTool::default(); |
| 553 |
for mode in BUILD_TOOL_CYCLE { |
| 554 |
tool.cycle(); |
| 555 |
assert_eq!(tool.mode, Some(mode)); |
| 556 |
} |
| 557 |
tool.cycle(); |
| 558 |
assert_eq!(tool.mode, None); |
| 559 |
} |
| 560 |
|
| 561 |
#[test] |
| 562 |
fn demolition_marks_structures_and_roofs_in_the_area() { |
| 563 |
let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass); |
| 564 |
map.set_roof(UVec2::new(2, 3), true); |
| 565 |
let mut blueprints = Blueprints::default(); |
| 566 |
let occupied = HashSet::from([UVec2::new(1, 1), UVec2::new(9, 9)]); |
| 567 |
mark_deconstruction( |
| 568 |
&map, |
| 569 |
&mut blueprints, |
| 570 |
&occupied, |
| 571 |
UVec2::ZERO, |
| 572 |
UVec2::splat(3), |
| 573 |
); |
| 574 |
assert_eq!(blueprints.0.len(), 2); |
| 575 |
assert_eq!(blueprints.get(UVec2::ONE), Some(BuildKind::Deconstruct)); |
| 576 |
assert_eq!( |
| 577 |
blueprints.get(UVec2::new(2, 3)), |
| 578 |
Some(BuildKind::Deconstruct) |
| 579 |
); |
| 580 |
} |
| 581 |
|
| 582 |
#[test] |
| 583 |
fn dragging_walls_plans_a_room_outline_on_free_ground() { |
| 584 |
let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass); |
| 585 |
map.set_plant(UVec2::new(2, 2), Some(crate::world::Plant::Tree)); |
| 586 |
let mut zones = Zones::default(); |
| 587 |
zones.add_tiles(&map, [UVec2::new(5, 2)]); |
| 588 |
let mut blueprints = Blueprints::default(); |
| 589 |
let occupied = HashSet::from([UVec2::new(5, 5)]); |
| 590 |
let tiles = placements(StructureKind::Wall, UVec2::new(5, 5), UVec2::new(2, 2)); |
| 591 |
assert_eq!(tiles.len(), 12); |
| 592 |
place_blueprints( |
| 593 |
&map, |
| 594 |
&mut zones, |
| 595 |
&mut blueprints, |
| 596 |
&occupied, |
| 597 |
StructureKind::Wall, |
| 598 |
&tiles, |
| 599 |
); |
| 600 |
assert_eq!(blueprints.0.len(), 10); |
| 601 |
assert!(!zones.is_stockpile(UVec2::new(5, 2))); |
| 602 |
assert_eq!( |
| 603 |
placements(StructureKind::Campfire, UVec2::ZERO, UVec2::ONE), |
| 604 |
vec![UVec2::ONE] |
| 605 |
); |
| 606 |
} |
| 607 |
|
| 608 |
#[test] |
| 609 |
fn shelter_is_a_wall_ring_with_one_door_around_the_beds() { |
| 610 |
let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 611 |
let beds = [UVec2::new(10, 10), UVec2::new(11, 10)]; |
| 612 |
let plan = ctx(&map, &[]).plan(&beds, UVec2::new(10, 2)).unwrap(); |
| 613 |
assert_eq!(plan.len(), 6 * 5 - 4 * 3); |
| 614 |
assert_eq!(count(&plan, DOOR), 1); |
| 615 |
let door = plan.iter().find(|(_, k)| **k == DOOR).unwrap().0; |
| 616 |
assert_eq!(door.y, 8); |
| 617 |
} |
| 618 |
|
| 619 |
|
| 620 |
#[test] |
| 621 |
fn rock_counts_as_wall() { |
| 622 |
let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 623 |
for y in 0..20 { |
| 624 |
map.set(UVec2::new(12, y), Terrain::Stone); |
| 625 |
} |
| 626 |
let plan = ctx(&map, &[]) |
| 627 |
.plan(&[UVec2::new(10, 10)], UVec2::new(10, 2)) |
| 628 |
.unwrap(); |
| 629 |
assert!(plan.keys().all(|t| t.x != 12)); |
| 630 |
assert_eq!(count(&plan, WALL) + count(&plan, DOOR), 16 - 5); |
| 631 |
} |
| 632 |
|
| 633 |
#[test] |
| 634 |
fn shelters_route_around_unbuildable_tiles() { |
| 635 |
let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 636 |
map.set(UVec2::new(10, 8), Terrain::ShallowWater); |
| 637 |
let plan = ctx(&map, &[]) |
| 638 |
.plan(&[UVec2::new(10, 10)], UVec2::new(10, 2)) |
| 639 |
.unwrap(); |
| 640 |
assert!(!plan.contains_key(&UVec2::new(10, 8))); |
| 641 |
assert_eq!( |
| 642 |
plan.values() |
| 643 |
.filter(|k| **k == BuildKind::Structure(StructureKind::Door)) |
| 644 |
.count(), |
| 645 |
1 |
| 646 |
); |
| 647 |
} |
| 648 |
|
| 649 |
|
| 650 |
|
| 651 |
#[test] |
| 652 |
fn walls_may_replace_empty_stockpile_tiles_but_not_stored_items() { |
| 653 |
let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); |
| 654 |
let mut zones = Zones::default(); |
| 655 |
zones.add_stockpile(&map, IVec2::new(12, 5), IVec2::new(15, 15)); |
| 656 |
let items = [UVec2::new(13, 10)]; |
| 657 |
let ctx = ShelterContext { |
| 658 |
map: &map, |
| 659 |
structures: &[], |
| 660 |
items: &items, |
| 661 |
}; |
| 662 |
let plan = ctx.plan(&[UVec2::new(11, 10)], UVec2::new(5, 10)).unwrap(); |
| 663 |
assert!(plan.keys().any(|t| zones.is_stockpile(*t))); |
| 664 |
assert!(!plan.contains_key(&UVec2::new(13, 10))); |
| 665 |
} |
| 666 |
|
| 667 |
#[test] |
| 668 |
fn enclosed_rooms_get_roof_blueprints() { |
| 669 |
let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass); |
| 670 |
for i in 3..=6 { |
| 671 |
for t in [ |
| 672 |
UVec2::new(i, 3), |
| 673 |
UVec2::new(i, 6), |
| 674 |
UVec2::new(3, i), |
| 675 |
UVec2::new(6, i), |
| 676 |
] { |
| 677 |
map.set_wall(t, true); |
| 678 |
} |
| 679 |
} |
| 680 |
map.set_wall(UVec2::new(4, 3), false); |
| 681 |
let structures = [(UVec2::new(4, 3), StructureKind::Door)]; |
| 682 |
let plan = ctx(&map, &structures) |
| 683 |
.plan(&[UVec2::new(4, 4)], UVec2::ZERO) |
| 684 |
.unwrap(); |
| 685 |
assert_eq!(plan.len(), 4); |
| 686 |
assert!(plan.values().all(|k| *k == BuildKind::Roof)); |
| 687 |
} |
| 688 |
|
| 689 |
#[test] |
| 690 |
fn open_areas_are_not_rooms() { |
| 691 |
let map = WorldMap::filled(UVec2::splat(30), Terrain::Grass); |
| 692 |
assert!(enclosed_room(&map, &HashSet::new(), UVec2::new(15, 15)).is_none()); |
| 693 |
} |
| 694 |
|
| 695 |
#[test] |
| 696 |
fn the_floor_tool_fills_an_area_but_skips_walls_and_water() { |
| 697 |
let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass); |
| 698 |
map.set_wall(UVec2::new(2, 2), true); |
| 699 |
map.set(UVec2::new(3, 3), Terrain::ShallowWater); |
| 700 |
map.set(UVec2::new(1, 1), Terrain::WoodFloor); |
| 701 |
let mut blueprints = Blueprints::default(); |
| 702 |
place_floors(&map, &mut blueprints, UVec2::new(3, 3), UVec2::ONE); |
| 703 |
assert_eq!(blueprints.0.len(), 9 - 3); |
| 704 |
assert!(blueprints.0.values().all(|k| *k == BuildKind::Floor)); |
| 705 |
} |
| 706 |
} |