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