Add chop/mine designations

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

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 22:05 UTC
commit
8279a8c0030fc6429a16849d45b3b835eea5af52
parent
ace548ce51
tree
browse at this commit

11 files changed +266 -24

CLAUDE.md +1 -0

@@ -31,6 +31,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
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 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 +- `src/designations.rs` `Designations(HashSet<tile>)` of trees/rock (`designatable` = site_work Chop/Mine) marked with the Chop/Mine tool (X; drag adds, shift-drag clears; yellow X gizmos); `prune_designations` drops worked-out tiles. choose_jobs works designated sites (find_work with a site predicate) right after food gathering, regardless of stock goals. Tools (zone, build, designate) cancel each other.
34 35 - 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 36 - Camera zoom buttons: Action::ZoomIn/ZoomOut (=/-) → `button_zoom` using `CameraController::zoomed`.
36 37 - `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.

src/actions.rs +4 -1

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

src/building.rs +4 -3

@@ -376,9 +376,10 @@ fn build_tool(
376 376 for action in actions.read() {
377 377 match action {
378 378 Action::CycleBuildTool => tool.cycle(),
379 - Action::Cancel | Action::ToggleStockpileTool | Action::ToggleFarmTool => {
380 - tool.kind = None
381 - }
379 + Action::Cancel
380 + | Action::ToggleStockpileTool
381 + | Action::ToggleFarmTool
382 + | Action::ToggleDesignateTool => tool.kind = None,
382 383 _ => continue,
383 384 }
384 385 tool.drag_start = None;

src/designations.rs +182 -0

@@ -0,0 +1,182 @@
1 +use std::collections::HashSet;
2 +
3 +use bevy::prelude::*;
4 +
5 +use crate::{
6 + actions::Action,
7 + jobs::{WorkKind, site_work},
8 + selection::{Cursor, update_cursor},
9 + sim::{SimSystems, SimTick},
10 + ui::PointerOverUi,
11 + world::{TILE_SIZE, WorldMap},
12 + worldgen::WorldGenerated,
13 + zones::{PREVIEW_ADD_COLOR, PREVIEW_REMOVE_COLOR, removing},
14 +};
15 +
16 +pub struct DesignationsPlugin;
17 +
18 +impl Plugin for DesignationsPlugin {
19 + fn build(&self, app: &mut App) {
20 + app.init_resource::<Designations>()
21 + .init_resource::<DesignateTool>()
22 + .add_systems(
23 + Update,
24 + (
25 + clear_designations.run_if(on_message::<WorldGenerated>),
26 + draw_designations,
27 + ),
28 + )
29 + .add_systems(
30 + PostUpdate,
31 + (designate_tool, draw_designate_preview)
32 + .chain()
33 + .after(update_cursor),
34 + )
35 + .add_systems(FixedUpdate, prune_designations.in_set(SimSystems::Decide));
36 + }
37 +}
38 +
39 +const PRUNE_INTERVAL_TICKS: u64 = 60;
40 +const DESIGNATION_COLOR: Color = Color::srgba(1.0, 0.85, 0.2, 0.9);
41 +
42 +#[derive(Resource, Default, Debug)]
43 +pub struct Designations(pub HashSet<UVec2>);
44 +
45 +#[derive(Resource, Default, Debug)]
46 +pub struct DesignateTool {
47 + pub active: bool,
48 + drag_start: Option<UVec2>,
49 +}
50 +
51 +pub fn designatable(map: &WorldMap, tile: UVec2) -> bool {
52 + site_work(map, tile).is_some_and(|w| matches!(w.kind, WorkKind::Chop | WorkKind::Mine))
53 +}
54 +
55 +pub fn designate(map: &WorldMap, designations: &mut Designations, start: UVec2, end: UVec2) {
56 + let (lo, hi) = (start.min(end), start.max(end));
57 + for y in lo.y..=hi.y {
58 + for x in lo.x..=hi.x {
59 + let tile = UVec2::new(x, y);
60 + if designatable(map, tile) {
61 + designations.0.insert(tile);
62 + }
63 + }
64 + }
65 +}
66 +
67 +fn clear_designations(mut designations: ResMut<Designations>) {
68 + designations.0.clear();
69 +}
70 +
71 +fn prune_designations(
72 + tick: Res<SimTick>,
73 + map: Res<WorldMap>,
74 + mut designations: ResMut<Designations>,
75 +) {
76 + if tick.0.is_multiple_of(PRUNE_INTERVAL_TICKS) && !designations.0.is_empty() {
77 + designations.0.retain(|&tile| designatable(&map, tile));
78 + }
79 +}
80 +
81 +#[allow(clippy::too_many_arguments)]
82 +fn designate_tool(
83 + mut actions: MessageReader<Action>,
84 + keys: Res<ButtonInput<KeyCode>>,
85 + buttons: Res<ButtonInput<MouseButton>>,
86 + over_ui: Res<PointerOverUi>,
87 + cursor: Res<Cursor>,
88 + map: Res<WorldMap>,
89 + mut tool: ResMut<DesignateTool>,
90 + mut designations: ResMut<Designations>,
91 +) {
92 + for action in actions.read() {
93 + match action {
94 + Action::ToggleDesignateTool => tool.active = !tool.active,
95 + Action::Cancel
96 + | Action::ToggleStockpileTool
97 + | Action::ToggleFarmTool
98 + | Action::CycleBuildTool => tool.active = false,
99 + _ => continue,
100 + }
101 + tool.drag_start = None;
102 + return;
103 + }
104 + if !tool.active {
105 + return;
106 + }
107 + if buttons.just_pressed(MouseButton::Left) && !over_ui.0 {
108 + tool.drag_start = cursor.tile;
109 + }
110 + if !buttons.just_released(MouseButton::Left) {
111 + return;
112 + }
113 + let (Some(start), Some(end)) = (tool.drag_start.take(), cursor.tile) else {
114 + return;
115 + };
116 + if removing(&keys) {
117 + let (lo, hi) = (start.min(end), start.max(end));
118 + designations
119 + .0
120 + .retain(|t| !(t.cmpge(lo).all() && t.cmple(hi).all()));
121 + } else {
122 + designate(&map, &mut designations, start, end);
123 + }
124 +}
125 +
126 +fn draw_designate_preview(
127 + keys: Res<ButtonInput<KeyCode>>,
128 + cursor: Res<Cursor>,
129 + tool: Res<DesignateTool>,
130 + mut gizmos: Gizmos,
131 +) {
132 + let (Some(start), Some(end)) = (tool.drag_start, cursor.tile) else {
133 + return;
134 + };
135 + let (min, max) = (start.min(end), start.max(end));
136 + let center = (WorldMap::tile_center(min) + WorldMap::tile_center(max)) / 2.0;
137 + let size = (max - min + 1).as_vec2() * TILE_SIZE;
138 + let color = if removing(&keys) {
139 + PREVIEW_REMOVE_COLOR
140 + } else {
141 + PREVIEW_ADD_COLOR
142 + };
143 + gizmos.rect_2d(center, size, color);
144 +}
145 +
146 +fn draw_designations(designations: Res<Designations>, mut gizmos: Gizmos) {
147 + let half = TILE_SIZE * 0.3;
148 + for &tile in &designations.0 {
149 + let c = WorldMap::tile_center(tile);
150 + gizmos.line_2d(
151 + c - Vec2::splat(half),
152 + c + Vec2::splat(half),
153 + DESIGNATION_COLOR,
154 + );
155 + gizmos.line_2d(
156 + c + Vec2::new(-half, half),
157 + c + Vec2::new(half, -half),
158 + DESIGNATION_COLOR,
159 + );
160 + }
161 +}
162 +
163 +#[cfg(test)]
164 +mod tests {
165 + use super::*;
166 + use crate::world::{Plant, Terrain};
167 +
168 + #[test]
169 + fn only_trees_and_rock_can_be_designated() {
170 + let mut map = WorldMap::filled(UVec2::splat(6), Terrain::Grass);
171 + map.set_plant(UVec2::new(1, 1), Some(Plant::Tree));
172 + map.set(UVec2::new(2, 2), Terrain::Stone);
173 + map.set_plant(UVec2::new(3, 3), Some(Plant::BerryBush));
174 + map.set_growth(UVec2::new(3, 3), 1.0);
175 + let mut designations = Designations::default();
176 + designate(&map, &mut designations, UVec2::ZERO, UVec2::splat(5));
177 + assert_eq!(
178 + designations.0,
179 + HashSet::from([UVec2::new(1, 1), UVec2::new(2, 2)])
180 + );
181 + }
182 +}

src/jobs/mod.rs +22 -9

@@ -10,6 +10,7 @@ use crate::{
10 10 animals::{Animal, AnimalKind},
11 11 building::{Blueprints, BuildKind},
12 12 colonist::{Colonist, Path, repath},
13 + designations::Designations,
13 14 economy::{ColonyGoals, in_colony},
14 15 family::{Child, Family, Homes},
15 16 food::{EAT_TICKS, Hunger, STARVING, find_food, portion, wants_food},
@@ -368,7 +369,7 @@ pub fn find_work(
368 369 map: &WorldMap,
369 370 reservations: &Reservations,
370 371 from: UVec2,
371 - wanted: impl Fn(ItemKind) -> bool,
372 + wanted: impl Fn(UVec2, ItemKind) -> bool,
372 373 ) -> Option<Task> {
373 374 let mut found = None;
374 375 nearest_tile(map, from, SEARCH_TILES, |stand| {
@@ -379,11 +380,13 @@ pub fn find_work(
379 380 }
380 381 let site = site.as_uvec2();
381 382 let work = site_work(map, site)?;
382 - (wanted(work.item) && reservations.is_free(Target::Site(site))).then_some(Task::Work {
383 - site,
384 - kind: work.kind,
385 - progress: 0,
386 - })
383 + (wanted(site, work.item) && reservations.is_free(Target::Site(site))).then_some(
384 + Task::Work {
385 + site,
386 + kind: work.kind,
387 + progress: 0,
388 + },
389 + )
387 390 });
388 391 found.is_some()
389 392 })?;
@@ -799,8 +802,9 @@ fn choose_jobs(
799 802 ),
800 803 With<Colonist>,
801 804 >,
802 - homes: Res<Homes>,
805 + plans: (Res<Homes>, Res<Designations>),
803 806 ) {
807 + let (homes, designations) = plans;
804 808 let wildlife: Vec<(Entity, UVec2, AnimalKind)> = animals
805 809 .iter()
806 810 .map(|(e, pos, a)| (e, pos.0, a.kind))
@@ -933,7 +937,16 @@ fn choose_jobs(
933 937 })
934 938 .flatten()
935 939 })
936 - .or_else(|| find_work(&map, &reservations, tile.0, |k| k.is_food() && wanted(k)))
940 + .or_else(|| find_work(&map, &reservations, tile.0, |_, k| k.is_food() && wanted(k)))
941 + .or_else(|| {
942 + (!designations.0.is_empty())
943 + .then(|| {
944 + find_work(&map, &reservations, tile.0, |site, _| {
945 + designations.0.contains(&site)
946 + })
947 + })
948 + .flatten()
949 + })
937 950 .or_else(|| {
938 951 wanted(ItemKind::Vegetables)
939 952 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
@@ -941,7 +954,7 @@ fn choose_jobs(
941 954 })
942 955 .or_else(|| find_bed_site(&build, adults, tile.0))
943 956 .or_else(|| find_blueprint_work(&build, tile.0))
944 - .or_else(|| find_work(&map, &reservations, tile.0, wanted))
957 + .or_else(|| find_work(&map, &reservations, tile.0, |_, k| wanted(k)))
945 958 .or_else(|| {
946 959 wanted(ItemKind::Meat)
947 960 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))

src/jobs/tests.rs +17 -0

@@ -21,6 +21,7 @@ fn test_app(map: WorldMap) -> App {
21 21 .init_resource::<Time>()
22 22 .init_resource::<Reservations>()
23 23 .init_resource::<crate::family::Homes>()
24 + .init_resource::<crate::designations::Designations>()
24 25 .init_resource::<ColonyGoals>()
25 26 .init_resource::<ItemAssets>()
26 27 .init_resource::<StructureAssets>()
@@ -1180,3 +1181,19 @@ fn colonists_refuel_a_dying_campfire_instead_of_building_a_new_one() {
1180 1181 assert_eq!(fuel.len(), 1);
1181 1182 assert!(fuel[0] > full / 2, "{fuel:?}");
1182 1183 }
1184 +
1185 +#[test]
1186 +fn designated_trees_are_cut_even_when_wood_is_not_wanted() {
1187 + let marked = UVec2::new(12, 5);
1188 + let mut app = test_app(map_with_trees(&[UVec2::new(4, 5), marked]));
1189 + set_goals(&mut app, &[]);
1190 + app.world_mut()
1191 + .resource_mut::<crate::designations::Designations>()
1192 + .0
1193 + .insert(marked);
1194 + spawn_colonist(&mut app, UVec2::new(2, 5));
1195 + run_ticks(&mut app, 61 + 60 * 3 + WorkKind::Chop.work());
1196 + let map = app.world().resource::<WorldMap>();
1197 + assert!(map.plant(marked.as_ivec2()).is_none());
1198 + assert_eq!(map.plant(IVec2::new(4, 5)), Some(Plant::Tree));
1199 +}

src/main.rs +2 -0

@@ -3,6 +3,7 @@ mod animals;
3 3 mod building;
4 4 mod camera;
5 5 mod colonist;
6 +mod designations;
6 7 mod economy;
7 8 mod family;
8 9 mod farming;
@@ -66,6 +67,7 @@ fn main() {
66 67 economy::EconomyPlugin,
67 68 population::PopulationPlugin,
68 69 family::FamilyPlugin,
70 + designations::DesignationsPlugin,
69 71 plants::PlantsPlugin,
70 72 selection::SelectionPlugin,
71 73 zones::ZonesPlugin,

src/selection.rs +11 -5

@@ -6,6 +6,7 @@ use crate::{
6 6 building::{Blueprints, BuildTool},
7 7 camera::CameraController,
8 8 colonist::{Colonist, Path},
9 + designations::DesignateTool,
9 10 economy::{ColonyGoals, Stock, in_colony},
10 11 family::{Child, Partner},
11 12 food::Hunger,
@@ -34,9 +35,11 @@ impl Plugin for SelectionPlugin {
34 35 PostUpdate,
35 36 (
36 37 update_cursor,
37 - update_selection.run_if(|tool: Res<ZoneTool>, build: Res<BuildTool>| {
38 - !tool.active() && !build.active()
39 - }),
38 + update_selection.run_if(
39 + |tool: Res<ZoneTool>, build: Res<BuildTool>, mark: Res<DesignateTool>| {
40 + !tool.active() && !build.active() && !mark.active
41 + },
42 + ),
40 43 issue_move_orders,
41 44 draw_highlights,
42 45 update_tile_info,
@@ -260,7 +263,7 @@ fn update_tile_info(
260 263 goals: Res<ColonyGoals>,
261 264 log: Res<ColonyLog>,
262 265 zones: Res<Zones>,
263 - tools: (Res<ZoneTool>, Res<BuildTool>),
266 + tools: (Res<ZoneTool>, Res<BuildTool>, Res<DesignateTool>),
264 267 clock: (
265 268 Res<Time<Virtual>>,
266 269 Res<SimTick>,
@@ -359,8 +362,11 @@ fn update_tile_info(
359 362 )
360 363 .collect();
361 364 let events: String = log.0.iter().map(|line| format!("\n{line}")).collect();
362 - let (tool, build_tool) = tools;
365 + let (tool, build_tool, designate_tool) = tools;
363 366 let tool_hint = match (tool.mode, build_tool.kind) {
367 + _ if designate_tool.active => {
368 + "\nCHOP/MINE: drag over trees and rock, shift-drag to clear".to_string()
369 + }
364 370 (Some(ZoneKind::Stockpile), _) => {
365 371 "\nSTOCKPILE TOOL: drag to add, shift-drag to remove".to_string()
366 372 }

src/ui.rs +19 -4

@@ -11,6 +11,7 @@ use bevy::{
11 11 use crate::{
12 12 actions::Action,
13 13 building::BuildTool,
14 + designations::DesignateTool,
14 15 sim::SimSpeed,
15 16 zones::{ZoneKind, ZoneTool},
16 17 };
@@ -35,7 +36,7 @@ impl Plugin for HudPlugin {
35 36 }
36 37 }
37 38
38 -const TOOLBAR: [Action; 13] = [
39 +const TOOLBAR: [Action; 14] = [
39 40 Action::TogglePause,
40 41 Action::SetSpeed(SimSpeed::Normal),
41 42 Action::SetSpeed(SimSpeed::Fast),
@@ -44,6 +45,7 @@ const TOOLBAR: [Action; 13] = [
44 45 Action::ToggleStockpileTool,
45 46 Action::ToggleFarmTool,
46 47 Action::CycleBuildTool,
48 + Action::ToggleDesignateTool,
47 49 Action::Cancel,
48 50 Action::NewWorld,
49 51 Action::ZoomIn,
@@ -196,9 +198,11 @@ pub fn is_active(
196 198 speed: SimSpeed,
197 199 tool: Option<ZoneKind>,
198 200 building: bool,
201 + designating: bool,
199 202 ) -> bool {
200 203 match action {
201 204 Action::CycleBuildTool => building,
205 + Action::ToggleDesignateTool => designating,
202 206 Action::TogglePause => paused,
203 207 Action::SetSpeed(s) => !paused && s == speed,
204 208 Action::ToggleStockpileTool => tool == Some(ZoneKind::Stockpile),
@@ -212,6 +216,7 @@ fn style_buttons(
212 216 speed: Res<SimSpeed>,
213 217 tool: Res<ZoneTool>,
214 218 build_tool: Res<BuildTool>,
219 + designate_tool: Res<DesignateTool>,
215 220 mut buttons: Query<(&ActionButton, &Hovered, Has<Pressed>, &mut BackgroundColor)>,
216 221 ) {
217 222 for (button, hovered, pressed, mut color) in &mut buttons {
@@ -221,6 +226,7 @@ fn style_buttons(
221 226 *speed,
222 227 tool.mode,
223 228 build_tool.active(),
229 + designate_tool.active,
224 230 );
225 231 let new = match (pressed && hovered.get(), active, hovered.get()) {
226 232 (true, ..) => BUTTON_PRESSED_COLOR,
@@ -257,16 +263,24 @@ mod tests {
257 263 #[test]
258 264 fn active_buttons_reflect_simulation_state() {
259 265 let normal = SimSpeed::Normal;
260 - assert!(is_active(Action::TogglePause, true, normal, None, false));
266 + assert!(is_active(
267 + Action::TogglePause,
268 + true,
269 + normal,
270 + None,
271 + false,
272 + false
273 + ));
261 274 let fast = Action::SetSpeed(SimSpeed::Fast);
262 - assert!(is_active(fast, false, SimSpeed::Fast, None, false));
263 - assert!(!is_active(fast, true, SimSpeed::Fast, None, false));
275 + assert!(is_active(fast, false, SimSpeed::Fast, None, false, false));
276 + assert!(!is_active(fast, true, SimSpeed::Fast, None, false, false));
264 277 let farm = Some(ZoneKind::Farm);
265 278 assert!(is_active(
266 279 Action::ToggleFarmTool,
267 280 false,
268 281 normal,
269 282 farm,
283 + false,
270 284 false
271 285 ));
272 286 assert!(!is_active(
@@ -274,6 +288,7 @@ mod tests {
274 288 false,
275 289 normal,
276 290 farm,
291 + false,
277 292 false
278 293 ));
279 294 }

src/zones.rs +3 -1

@@ -260,7 +260,9 @@ 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 | Action::CycleBuildTool => tool.mode = None,
263 + Action::Cancel | Action::CycleBuildTool | Action::ToggleDesignateTool => {
264 + tool.mode = None
265 + }
264 266 _ => continue,
265 267 }
266 268 tool.drag_start = None;

tasks.md +1 -1

@@ -28,6 +28,7 @@
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 30 - [x] Refuelling fires; manual build tool (B: wall room outlines, doors, campfires, torches, beds); zoom buttons
31 +- [x] Designations: Chop/Mine tool (X) marks trees and rock for anyone to work
31 32 - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map
32 33
33 34 ## Planned
@@ -36,7 +37,6 @@
36 37 - [ ] Per-colonist work priorities (busy colonies leave low-priority work such as hauling undone)
37 38 - [ ] Walls block light; indoor darkness; deconstruction
38 39 - [ ] Stockpile filters (which item kinds a zone accepts) and storage priority
39 -- [ ] Designations (mark trees/rock for anyone to work) and per-colonist work priorities (job order is fixed: deliver > found/expand stockpile > chop/mine > haul > wander)
40 40 - [ ] UI to view/override learned stock targets
41 41 - [ ] Sim performance: per-tile scans in `BuildContext::buildable` and site searches; wasm ticks still ~1 ms each with a full colony
42 42 - [ ] Smoother hill shading at close zoom (per-tile shading looks blocky)