Add a manual build tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 22:03 UTC
commit
ace548ce51cb1586dcee9e8d559204317ad01a1e
parent
5531aaf964
tree
browse at this commit

7 files changed +264 -25

CLAUDE.md +4 -0

@@ -30,6 +30,9 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
30 30 - `src/structures.rs` `Structure{kind: StructureKind::{Campfire, Torch, Bed, Wall, Door}}` + TilePos entities; `Fuel(u32)` only on kinds with `fuel_ticks()` (fires: 12h/8h), `burn_fuel` (SimSystems::Act) despawns at 0; beds are permanent. Kind stats: `cost()` (material, amount): fires/bed/door wood, wall 2 stone; `light()` radius+color (fires only), `name()`. Visuals are Mesh2d children (campfire = stone ring + crossed logs + triangle flame, torch = stick + small flame, bed = frame + blanket + pillow, wall = stone square, door = brown square + knob; avoid plain circles, those read as colonists; z=0.8). `spawn_structure` inserts Fuel when needed. Cleared on WorldGenerated.
31 31 - `src/needs.rs` `Rest(f32)` on colonists (1 = rested): `update_rest` (SimSystems::Act) drains 1/24 per game hour awake, recovers 1/7 per hour (1/5 in a bed, ×ROOF_REST_BONUS 1.25 under a roof) while `is_sleeping` (Sleep job, at spot, path empty). `wants_sleep`: rest < NAP_REST (0.3) any time, or night (21–6) and rest < BEDTIME_REST (0.9). `done_sleeping`: rest full and not night (sleep lasts until morning). EXHAUSTED_REST (0.1) makes choose_jobs drop autonomous (not ordered) jobs. `SleepContext::spot` = nearest bed not reserved as `Target::SleepSpot`, else nearest free tile (dry, no plant/stockpile/fire/item, unreserved `Target::SleepSpot`) to the nearest campfire, else home. Sleeping colonists show a `SleepMarker` Text2d "z" child.
32 32 - `src/building.rs` construction planning. `BuildKind::{Structure(StructureKind), Roof, Pave}` with `cost()`/`work()`/`blocks_tile()` (walls). `Blueprints(HashMap<tile, BuildKind>)` resource, drawn as gizmo outlines, cleared on WorldGenerated. `plan_shelter` (SimSystems::Decide, every 300 ticks, only while no non-Pave blueprints; merges its plan into Blueprints): for unroofed beds, if `enclosed_room` (4-conn flood bounded by walls/doors/rock, ≤ MAX_ROOM_TILES, not touching map edge) → Roof blueprints for unroofed room tiles; else `ShelterContext::plan` tries every wall ring with 0–2 tiles margin per side around the beds' bbox, cheapest (ring length + penalty per margin-0 side) where every ring tile is rock/existing wall/door or buildable (dry, no plant/item/structure; empty stockpile tiles allowed and removed from the stockpile when planned), one Door on the non-corner ring tile nearest the campfire/home.
33 +- Build tool (building.rs): `BuildTool{kind}` cycled by B / Build button through `BUILD_TOOL_CYCLE` (Wall, Door, Campfire, Torch, Bed, off); Wall drag = room outline (`placements`), others place at the release tile; `place_blueprints` skips plants/walls/structures/water/rock and clears stockpile under walls/doors; shift-drag removes blueprints in the rect. Zone tool and build tool cancel each other; selection ignores clicks while either is active. Colonists build manual blueprints via `find_blueprint_work`.
34 +- Refuel: `BuildKind::Refuel(kind)` (cost = kind.cost(), 60 work) reuses the Build task; `find_refuel` (before find_lighting) picks the lowest-fuel fire within 2×HOME_RADIUS of home below 1/REFUEL_BELOW_SHARE of full; completion resets Fuel to full.
35 +- Camera zoom buttons: Action::ZoomIn/ZoomOut (=/-) → `button_zoom` using `CameraController::zoomed`.
33 36 - `src/food.rs` spoilage: `spoil_food` (every 60 ticks) adds `spoilage(kind, roofed, ticks)` to food stacks and carried food (shelf life meat 2 d, berries 3, meals 4, vegetables 5; ×0.5 under a roof); spoil ≥ 1 removes it. `Hunger(f32)` on colonists (1 = fed): `update_hunger` drains 1/24 per game hour (half while sleeping). HUNGRY 0.35 → Eat job; STARVING 0.05 interrupts autonomous work like exhaustion, but only when unreserved food exists (`food_to_fetch`), else colonists thrashed their job every tick. `find_food` prefers Meal stacks, then raw food; `portion(kind, fed)` = units to fill up. Eat: walk to pickup, take portion into `Carrying`, eat EAT_TICKS, add nutrition.
34 37 - `src/plants.rs` `grow_plants` every GROWTH_INTERVAL_TICKS: plants with `ripening_ticks` (BerryBush 2 days, Crop 1.5) gain growth × `fertility(terrain)` (Soil 1, Grass .8, Sand .4, Gravel .3; wild bushes ≥ .8); writes TileChanged when `PlantLook` stage changes.
35 38 - `src/animals.rs` `Animal{kind: AnimalKind::{Deer, Rabbit}}` entities (Path/TilePos/SimPos/MoveSpeed, moved by colonist `follow_path`; ellipse-body visuals, z=0.9). `populate` on WorldGenerated (population per kind, ≥25 tiles from colony), `migrate` refills one per kind every 2 game hours, `wander` idles 2–8 s then walks ≤10 tiles; an animal reserved as `Target::Animal` (being hunted) stands still. meat 30/8, hunt_work 360/160.
@@ -51,6 +54,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
51 54 - Shift-held drags can't be driven in the browser pane (computer-tool modifiers / synthetic Shift keydown don't reach Bevy as a held key during a real drag); test modifier logic headlessly.
52 55 - Browser verification: `.claude/launch.json` config `web` (python http.server :8080 serving target/web) → `preview_start name=web`, then screenshot/scroll in built-in browser. Synthetic JS pointer events are ignored by winit; real drags only via computer tool left_click_drag (left button) — temporarily bind Left to test drag code. computer tool `right_click` works for RMB clicks. Pause (Space) before clicking moving colonists.
53 56 - Browser pane screenshots can lag 1-2s behind the canvas (verified: rAF gaps ≤51ms while screenshot still showed old frame). Wait ≥3s after an action, or cross-check via hover text before concluding a render bug.
57 +- Repeated computer-tool clicks on the same UI button without moving the pointer register only once (pre-existing, affects all toggles); hover elsewhere between clicks or use the key.
54 58 - Bevy logs in browser console; check for ERROR after load. Console log persists across reloads in the pane — count "Creating new window" lines to tell which load an error belongs to.
55 59
56 60 ## Docs

src/actions.rs +4 -1

@@ -17,6 +17,7 @@ pub enum Action {
17 17 SetSpeed(SimSpeed),
18 18 ToggleStockpileTool,
19 19 ToggleFarmTool,
20 + CycleBuildTool,
20 21 Cancel,
21 22 NewWorld,
22 23 ToggleUi,
@@ -24,7 +25,7 @@ pub enum Action {
24 25 ZoomOut,
25 26 }
26 27
27 -pub const KEYBINDS: [(KeyCode, Action); 12] = [
28 +pub const KEYBINDS: [(KeyCode, Action); 13] = [
28 29 (KeyCode::Space, Action::TogglePause),
29 30 (KeyCode::Digit1, Action::SetSpeed(SimSpeed::Normal)),
30 31 (KeyCode::Digit2, Action::SetSpeed(SimSpeed::Fast)),
@@ -32,6 +33,7 @@ pub const KEYBINDS: [(KeyCode, Action); 12] = [
32 33 (KeyCode::Digit4, Action::SetSpeed(SimSpeed::Max)),
33 34 (KeyCode::KeyZ, Action::ToggleStockpileTool),
34 35 (KeyCode::KeyF, Action::ToggleFarmTool),
36 + (KeyCode::KeyB, Action::CycleBuildTool),
35 37 (KeyCode::Escape, Action::Cancel),
36 38 (KeyCode::KeyR, Action::NewWorld),
37 39 (KeyCode::KeyH, Action::ToggleUi),
@@ -49,6 +51,7 @@ impl Action {
49 51 Action::SetSpeed(SimSpeed::Max) => "Max",
50 52 Action::ToggleStockpileTool => "Stockpile",
51 53 Action::ToggleFarmTool => "Farm",
54 + Action::CycleBuildTool => "Build",
52 55 Action::Cancel => "Cancel",
53 56 Action::NewWorld => "New world",
54 57 Action::ToggleUi => "Hide UI",

src/building.rs +194 -1

@@ -3,14 +3,17 @@ use std::collections::{HashMap, HashSet, VecDeque};
3 3 use bevy::prelude::*;
4 4
5 5 use crate::{
6 + actions::Action,
6 7 colonist::Colonist,
7 8 family::Homes,
8 9 items::{Item, ItemKind},
10 + selection::{Cursor, update_cursor},
9 11 sim::{SimSystems, SimTick},
10 12 structures::{Structure, StructureKind},
13 + ui::PointerOverUi,
11 14 world::{TILE_SIZE, TilePos, WorldMap},
12 15 worldgen::WorldGenerated,
13 - zones::{Zones, can_zone},
16 + zones::{PREVIEW_ADD_COLOR, PREVIEW_REMOVE_COLOR, Zones, can_zone, removing},
14 17 };
15 18
16 19 pub struct BuildingPlugin;
@@ -25,6 +28,13 @@ impl Plugin for BuildingPlugin {
25 28 draw_blueprints,
26 29 ),
27 30 )
31 + .init_resource::<BuildTool>()
32 + .add_systems(
33 + PostUpdate,
34 + (build_tool, draw_build_preview)
35 + .chain()
36 + .after(update_cursor),
37 + )
28 38 .add_systems(FixedUpdate, plan_shelter.in_set(SimSystems::Decide));
29 39 }
30 40 }
@@ -284,6 +294,152 @@ pub fn plan_shelter(
284 294 }
285 295 }
286 296
297 +pub const BUILD_TOOL_CYCLE: [StructureKind; 5] = [
298 + StructureKind::Wall,
299 + StructureKind::Door,
300 + StructureKind::Campfire,
301 + StructureKind::Torch,
302 + StructureKind::Bed,
303 +];
304 +
305 +#[derive(Resource, Default, Debug)]
306 +pub struct BuildTool {
307 + pub kind: Option<StructureKind>,
308 + drag_start: Option<UVec2>,
309 +}
310 +
311 +impl BuildTool {
312 + pub fn active(&self) -> bool {
313 + self.kind.is_some()
314 + }
315 +
316 + fn cycle(&mut self) {
317 + let next = match self.kind {
318 + None => Some(0),
319 + Some(kind) => BUILD_TOOL_CYCLE
320 + .iter()
321 + .position(|k| *k == kind)
322 + .map(|i| i + 1)
323 + .filter(|&i| i < BUILD_TOOL_CYCLE.len()),
324 + };
325 + self.kind = next.map(|i| BUILD_TOOL_CYCLE[i]);
326 + }
327 +}
328 +
329 +pub fn placements(kind: StructureKind, start: UVec2, end: UVec2) -> Vec<UVec2> {
330 + if kind != StructureKind::Wall {
331 + return vec![end];
332 + }
333 + let (lo, hi) = (start.min(end), start.max(end));
334 + (lo.y..=hi.y)
335 + .flat_map(|y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
336 + .filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y)
337 + .collect()
338 +}
339 +
340 +pub fn place_blueprints(
341 + map: &WorldMap,
342 + zones: &mut Zones,
343 + blueprints: &mut Blueprints,
344 + occupied: &HashSet<UVec2>,
345 + kind: StructureKind,
346 + tiles: &[UVec2],
347 +) {
348 + for &tile in tiles {
349 + let free = can_zone(map, tile.as_ivec2())
350 + && map.plant(tile.as_ivec2()).is_none()
351 + && !map.is_wall(tile.as_ivec2())
352 + && !occupied.contains(&tile);
353 + if !free {
354 + continue;
355 + }
356 + if matches!(kind, StructureKind::Wall | StructureKind::Door) {
357 + zones.remove_tile(tile);
358 + }
359 + blueprints.0.insert(tile, BuildKind::Structure(kind));
360 + }
361 +}
362 +
363 +#[allow(clippy::too_many_arguments)]
364 +fn build_tool(
365 + mut actions: MessageReader<Action>,
366 + keys: Res<ButtonInput<KeyCode>>,
367 + buttons: Res<ButtonInput<MouseButton>>,
368 + over_ui: Res<PointerOverUi>,
369 + cursor: Res<Cursor>,
370 + map: Res<WorldMap>,
371 + mut tool: ResMut<BuildTool>,
372 + mut zones: ResMut<Zones>,
373 + mut blueprints: ResMut<Blueprints>,
374 + structures: Query<&TilePos, (With<Structure>, Without<Colonist>)>,
375 +) {
376 + for action in actions.read() {
377 + match action {
378 + Action::CycleBuildTool => tool.cycle(),
379 + Action::Cancel | Action::ToggleStockpileTool | Action::ToggleFarmTool => {
380 + tool.kind = None
381 + }
382 + _ => continue,
383 + }
384 + tool.drag_start = None;
385 + return;
386 + }
387 + let Some(kind) = tool.kind else {
388 + return;
389 + };
390 + if buttons.just_pressed(MouseButton::Left) && !over_ui.0 {
391 + tool.drag_start = cursor.tile;
392 + }
393 + if !buttons.just_released(MouseButton::Left) {
394 + return;
395 + }
396 + let (Some(start), Some(end)) = (tool.drag_start.take(), cursor.tile) else {
397 + return;
398 + };
399 + if removing(&keys) {
400 + let (lo, hi) = (start.min(end), start.max(end));
401 + blueprints
402 + .0
403 + .retain(|t, _| !(t.cmpge(lo).all() && t.cmple(hi).all()));
404 + return;
405 + }
406 + let occupied: HashSet<UVec2> = structures.iter().map(|p| p.0).collect();
407 + let tiles = placements(kind, start, end);
408 + place_blueprints(&map, &mut zones, &mut blueprints, &occupied, kind, &tiles);
409 +}
410 +
411 +fn draw_build_preview(
412 + keys: Res<ButtonInput<KeyCode>>,
413 + cursor: Res<Cursor>,
414 + tool: Res<BuildTool>,
415 + mut gizmos: Gizmos,
416 +) {
417 + let (Some(kind), Some(end)) = (tool.kind, cursor.tile) else {
418 + return;
419 + };
420 + let start = tool.drag_start.unwrap_or(end);
421 + let color = if removing(&keys) {
422 + PREVIEW_REMOVE_COLOR
423 + } else {
424 + PREVIEW_ADD_COLOR
425 + };
426 + let tiles = if removing(&keys) {
427 + let (lo, hi) = (start.min(end), start.max(end));
428 + (lo.y..=hi.y)
429 + .flat_map(|y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
430 + .collect()
431 + } else {
432 + placements(kind, start, end)
433 + };
434 + for tile in tiles {
435 + gizmos.rect_2d(
436 + WorldMap::tile_center(tile),
437 + Vec2::splat(TILE_SIZE * 0.7),
438 + color,
439 + );
440 + }
441 +}
442 +
287 443 fn draw_blueprints(mut gizmos: Gizmos, blueprints: Res<Blueprints>) {
288 444 for (tile, kind) in &blueprints.0 {
289 445 let color = match kind {
@@ -321,6 +477,43 @@ mod tests {
321 477 const DOOR: BuildKind = BuildKind::Structure(StructureKind::Door);
322 478
323 479 #[test]
480 + fn the_build_tool_cycles_through_structures_and_off() {
481 + let mut tool = BuildTool::default();
482 + for kind in BUILD_TOOL_CYCLE {
483 + tool.cycle();
484 + assert_eq!(tool.kind, Some(kind));
485 + }
486 + tool.cycle();
487 + assert_eq!(tool.kind, None);
488 + }
489 +
490 + #[test]
491 + fn dragging_walls_plans_a_room_outline_on_free_ground() {
492 + let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
493 + map.set_plant(UVec2::new(2, 2), Some(crate::world::Plant::Tree));
494 + let mut zones = Zones::default();
495 + zones.add_tiles(&map, [UVec2::new(5, 2)]);
496 + let mut blueprints = Blueprints::default();
497 + let occupied = HashSet::from([UVec2::new(5, 5)]);
498 + let tiles = placements(StructureKind::Wall, UVec2::new(5, 5), UVec2::new(2, 2));
499 + assert_eq!(tiles.len(), 12);
500 + place_blueprints(
501 + &map,
502 + &mut zones,
503 + &mut blueprints,
504 + &occupied,
505 + StructureKind::Wall,
506 + &tiles,
507 + );
508 + assert_eq!(blueprints.0.len(), 10);
509 + assert!(!zones.is_stockpile(UVec2::new(5, 2)));
510 + assert_eq!(
511 + placements(StructureKind::Campfire, UVec2::ZERO, UVec2::ONE),
512 + vec![UVec2::ONE]
513 + );
514 + }
515 +
516 + #[test]
324 517 fn shelter_is_a_wall_ring_with_one_door_around_the_beds() {
325 518 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
326 519 let beds = [UVec2::new(10, 10), UVec2::new(11, 10)];

src/selection.rs +21 -7

@@ -3,7 +3,7 @@ use bevy::{prelude::*, transform::TransformSystems, window::PrimaryWindow};
3 3 use crate::{
4 4 actions::Action,
5 5 animals::Animal,
6 - building::Blueprints,
6 + building::{Blueprints, BuildTool},
7 7 camera::CameraController,
8 8 colonist::{Colonist, Path},
9 9 economy::{ColonyGoals, Stock, in_colony},
@@ -34,7 +34,9 @@ impl Plugin for SelectionPlugin {
34 34 PostUpdate,
35 35 (
36 36 update_cursor,
37 - update_selection.run_if(|tool: Res<ZoneTool>| !tool.active()),
37 + update_selection.run_if(|tool: Res<ZoneTool>, build: Res<BuildTool>| {
38 + !tool.active() && !build.active()
39 + }),
38 40 issue_move_orders,
39 41 draw_highlights,
40 42 update_tile_info,
@@ -258,7 +260,7 @@ fn update_tile_info(
258 260 goals: Res<ColonyGoals>,
259 261 log: Res<ColonyLog>,
260 262 zones: Res<Zones>,
261 - tool: Res<ZoneTool>,
263 + tools: (Res<ZoneTool>, Res<BuildTool>),
262 264 clock: (
263 265 Res<Time<Virtual>>,
264 266 Res<SimTick>,
@@ -357,10 +359,22 @@ fn update_tile_info(
357 359 )
358 360 .collect();
359 361 let events: String = log.0.iter().map(|line| format!("\n{line}")).collect();
360 - let tool_hint = match tool.mode {
361 - Some(ZoneKind::Stockpile) => "\nSTOCKPILE TOOL: drag to add, shift-drag to remove",
362 - Some(ZoneKind::Farm) => "\nFARM TOOL: drag to add, shift-drag to remove",
363 - None => "",
362 + let (tool, build_tool) = tools;
363 + let tool_hint = match (tool.mode, build_tool.kind) {
364 + (Some(ZoneKind::Stockpile), _) => {
365 + "\nSTOCKPILE TOOL: drag to add, shift-drag to remove".to_string()
366 + }
367 + (Some(ZoneKind::Farm), _) => "\nFARM TOOL: drag to add, shift-drag to remove".to_string(),
368 + (None, Some(kind)) => format!(
369 + "\nBUILD {}: {}, shift-drag to remove plans, B for next",
370 + kind.name().to_uppercase(),
371 + if kind == StructureKind::Wall {
372 + "drag a room outline"
373 + } else {
374 + "click to place"
375 + }
376 + ),
377 + (None, None) => String::new(),
364 378 };
365 379 let info = format!(
366 380 "{clock} {speed}\nSeed: {} Colonists: {}\n{materials}{tool_hint}\nHover: {}{hovered_zone}{hovered_items}{hovered_structure}\nSelected: {selected}{events}",

src/ui.rs +36 -8

@@ -10,6 +10,7 @@ use bevy::{
10 10
11 11 use crate::{
12 12 actions::Action,
13 + building::BuildTool,
13 14 sim::SimSpeed,
14 15 zones::{ZoneKind, ZoneTool},
15 16 };
@@ -34,7 +35,7 @@ impl Plugin for HudPlugin {
34 35 }
35 36 }
36 37
37 -const TOOLBAR: [Action; 12] = [
38 +const TOOLBAR: [Action; 13] = [
38 39 Action::TogglePause,
39 40 Action::SetSpeed(SimSpeed::Normal),
40 41 Action::SetSpeed(SimSpeed::Fast),
@@ -42,6 +43,7 @@ const TOOLBAR: [Action; 12] = [
42 43 Action::SetSpeed(SimSpeed::Max),
43 44 Action::ToggleStockpileTool,
44 45 Action::ToggleFarmTool,
46 + Action::CycleBuildTool,
45 47 Action::Cancel,
46 48 Action::NewWorld,
47 49 Action::ZoomIn,
@@ -188,8 +190,15 @@ fn apply_ui_visibility(
188 190 }
189 191 }
190 192
191 -pub fn is_active(action: Action, paused: bool, speed: SimSpeed, tool: Option<ZoneKind>) -> bool {
193 +pub fn is_active(
194 + action: Action,
195 + paused: bool,
196 + speed: SimSpeed,
197 + tool: Option<ZoneKind>,
198 + building: bool,
199 +) -> bool {
192 200 match action {
201 + Action::CycleBuildTool => building,
193 202 Action::TogglePause => paused,
194 203 Action::SetSpeed(s) => !paused && s == speed,
195 204 Action::ToggleStockpileTool => tool == Some(ZoneKind::Stockpile),
@@ -202,10 +211,17 @@ fn style_buttons(
202 211 time: Res<Time<Virtual>>,
203 212 speed: Res<SimSpeed>,
204 213 tool: Res<ZoneTool>,
214 + build_tool: Res<BuildTool>,
205 215 mut buttons: Query<(&ActionButton, &Hovered, Has<Pressed>, &mut BackgroundColor)>,
206 216 ) {
207 217 for (button, hovered, pressed, mut color) in &mut buttons {
208 - let active = is_active(button.0, time.is_paused(), *speed, tool.mode);
218 + let active = is_active(
219 + button.0,
220 + time.is_paused(),
221 + *speed,
222 + tool.mode,
223 + build_tool.active(),
224 + );
209 225 let new = match (pressed && hovered.get(), active, hovered.get()) {
210 226 (true, ..) => BUTTON_PRESSED_COLOR,
211 227 (_, true, _) => BUTTON_ACTIVE_COLOR,
@@ -241,13 +257,25 @@ mod tests {
241 257 #[test]
242 258 fn active_buttons_reflect_simulation_state() {
243 259 let normal = SimSpeed::Normal;
244 - assert!(is_active(Action::TogglePause, true, normal, None));
260 + assert!(is_active(Action::TogglePause, true, normal, None, false));
245 261 let fast = Action::SetSpeed(SimSpeed::Fast);
246 - assert!(is_active(fast, false, SimSpeed::Fast, None));
247 - assert!(!is_active(fast, true, SimSpeed::Fast, None));
262 + assert!(is_active(fast, false, SimSpeed::Fast, None, false));
263 + assert!(!is_active(fast, true, SimSpeed::Fast, None, false));
248 264 let farm = Some(ZoneKind::Farm);
249 - assert!(is_active(Action::ToggleFarmTool, false, normal, farm));
250 - assert!(!is_active(Action::ToggleStockpileTool, false, normal, farm));
265 + assert!(is_active(
266 + Action::ToggleFarmTool,
267 + false,
268 + normal,
269 + farm,
270 + false
271 + ));
272 + assert!(!is_active(
273 + Action::ToggleStockpileTool,
274 + false,
275 + normal,
276 + farm,
277 + false
278 + ));
251 279 }
252 280
253 281 #[test]

src/zones.rs +4 -4

@@ -28,8 +28,8 @@ impl Plugin for ZonesPlugin {
28 28
29 29 pub const NEW_STOCKPILE_SIZE: u32 = 4;
30 30 const STOCKPILE_SITE_SEARCH_TILES: usize = 4000;
31 -const PREVIEW_ADD_COLOR: Color = Color::srgba(0.4, 0.7, 1.0, 0.9);
32 -const PREVIEW_REMOVE_COLOR: Color = Color::srgba(1.0, 0.4, 0.4, 0.9);
31 +pub const PREVIEW_ADD_COLOR: Color = Color::srgba(0.4, 0.7, 1.0, 0.9);
32 +pub const PREVIEW_REMOVE_COLOR: Color = Color::srgba(1.0, 0.4, 0.4, 0.9);
33 33
34 34 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35 35 pub enum ZoneKind {
@@ -242,7 +242,7 @@ pub fn find_stockpile_site(map: &WorldMap, near: UVec2) -> Option<UVec2> {
242 242 })
243 243 }
244 244
245 -fn removing(keys: &ButtonInput<KeyCode>) -> bool {
245 +pub fn removing(keys: &ButtonInput<KeyCode>) -> bool {
246 246 keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight])
247 247 }
248 248
@@ -260,7 +260,7 @@ fn zone_tool(
260 260 match action {
261 261 Action::ToggleStockpileTool => tool.toggle(ZoneKind::Stockpile),
262 262 Action::ToggleFarmTool => tool.toggle(ZoneKind::Farm),
263 - Action::Cancel => tool.mode = None,
263 + Action::Cancel | Action::CycleBuildTool => tool.mode = None,
264 264 _ => continue,
265 265 }
266 266 tool.drag_start = None;

tasks.md +1 -4

@@ -27,12 +27,10 @@
27 27 - [x] Performance: food/material searches use precollected tile sets and skip out-of-reach stacks; starving colonists without food keep working
28 28 - [x] Food security: stock counts only items within colony reach; harvesting, foraging and sowing take priority when food is short
29 29 - [x] Families: colonists who spend time together become partners, plan and build their own walled, roofed home with two beds, and have children who grow up after 4 days
30 +- [x] Refuelling fires; manual build tool (B: wall room outlines, doors, campfires, torches, beds); zoom buttons
30 31 - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map
31 32
32 33 ## Planned
33 -- [ ] Jobs: building (walls, floors; consumes materials)
34 -- [ ] Refuelling fires instead of rebuilding; manual build orders for fires
35 -- [ ] Manual wall/room building tool
36 34 - [ ] Mood, food poisoning from raw food; colonists leaving when unhappy; old age/disease
37 35 - [ ] Animals flee from hunters; tameable animals; butchering as a separate step
38 36 - [ ] Per-colonist work priorities (busy colonies leave low-priority work such as hauling undone)
@@ -43,4 +41,3 @@
43 41 - [ ] Sim performance: per-tile scans in `BuildContext::buildable` and site searches; wasm ticks still ~1 ms each with a full colony
44 42 - [ ] Smoother hill shading at close zoom (per-tile shading looks blocky)
45 43 - [ ] Colonist panel / better info UI (info panel is plain text)
46 -- [ ] Zoom +/- buttons (camera has no buttons; WASD/drag/scroll only)