Add a colonist panel with work assignments

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

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 22:27 UTC
commit
4ee48e06408ba977bc25f0c30c84301c9ef333ab
parent
2f6e9dbe99
tree
browse at this commit

9 files changed +387 -42

CLAUDE.md +1 -0

@@ -44,6 +44,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
44 44 - `src/population.rs` `ColonyLog` (last 4 events, shown in the info panel). `starve`: `Starvation` ticks at zero hunger; after STARVATION_TICKS (1 day) the colonist dies (drops load, releases reservations). `arrivals` daily at 10:00: if `prosperous` (≤ MAX_COLONISTS, stored nutrition ≥ 1 per colonist, a free bed or enough wood for one) and a 60% roll, a newcomer (`spawn_colonist`, next unused name) appears at an `edge_tile` with an unordered MoveTo home.
45 45 - `src/family.rs` families. `Family(u32)` (shared by partners and their children), `Partner(Entity)`, `Child{born}` (CHILD_SCALE visual, grows up after CHILDHOOD_TICKS = 4 days in `grow_up`) on colonists. `bond` (hourly): unpartnered adults not of the same family within BOND_RANGE accumulate `Affinity` hours per pair; ≥ BOND_HOURS + BOND_CHANCE roll → partners with a new family id; dangling Partner removed when the partner dies. `Homes{by_family: family → Home{lo, hi}}` (wall-ring corners, interior HOME_INTERIOR 4×3, `bed_tiles` 2 beds top-left, `owner(tile)`). `plan_homes` (every 300 ticks, Decide): drops homes of extinct families; roofs finished homes (`roofs_needed`: interior enclosed exactly); plans one home per partnered family without one via `HomeContext::plan` (BFS from colony home, center ≥ HOME_MIN_DISTANCE, core open + 1-tile belt free of stockpile/farm/blueprints/homes) → wall/door/bed blueprints (door nearest colony home). `births` (daily 20:00): family with 2 living partners, built home (roofed, 2 beds), < MAX_CHILDREN, colony < MAX_COLONISTS and food ≥ colonists, BIRTH_CHANCE → child spawned inside home. Children: choose_jobs gives them only sleep/eat/wander; `find_bed_site` counts adults. Sleep: own home bed (adults), free roofed home floor (children), else communal beds (`Homes::owner` None) → campfire. `plan_shelter` ignores beds inside homes. Selection text shows partner / child days left.
46 46 - `src/mood.rs` `Mood(f32)` (default 0.6) eases hourly (1/MOOD_HOURS) toward `mood_target(MoodFactors)`: hunger, rest, last meal cooked/raw within a day, `Sick`, partner, built family home, grief (a `Losses` tick within 2 days; starvation and old age push losses). `Ate{colonist, kind}` message from run_jobs' Eat completion → `digest`: `LastMeal`, raw food rolls `poison_chance` (meat 15%, berries/veg 4%) → `Sick{until}` 12 h (work rate halved in run_jobs). `leave`: adults below LEAVE_MOOD for LEAVE_AFTER_HOURS (48, `Unhappy` hours) leave (despawn, drop load). `Age{born}` (1 day = 1 year; `assign_age` gives new adults 18–40, children 0); `old_age` daily: from OLD_AGE 60, death chance 3%/year over. Selection shows mood/age/sick.
47 +- `src/panel.rs` top-right colonist panel: roster of `ColonistButton`s (rebuilt when names/child/mood face change; click selects) and, for a selected adult, `WorkToggle` buttons per `WorkType::{Food, Build, Gather, Haul, Tame}` toggling the colonist's `WorkPriorities` (required on Colonist; red = disabled). choose_jobs' autonomous candidates are a `(WorkType, finder)` list filtered by `allows`; orders ignore priorities.
47 48 - `src/rng.rs` `SimRng` SplitMix64 resource, seeded (deterministic sim; no `rand` dep).
48 49
49 50 ## Gotchas

src/colonist.rs +12 -2

@@ -5,7 +5,7 @@ use bevy::prelude::*;
5 5 use crate::{
6 6 food::Hunger,
7 7 items::{Carrying, ItemAssets},
8 - jobs::{CurrentJob, IdleTimer},
8 + jobs::{CurrentJob, IdleTimer, WorkPriorities},
9 9 needs::{Rest, sleep_marker},
10 10 pathfinding::find_path,
11 11 population::Starvation,
@@ -80,7 +80,17 @@ pub fn spawn_colonist(
80 80
81 81 #[derive(Component, Debug)]
82 82 #[require(
83 - Transform, SimPos, TilePos, Path, CurrentJob, IdleTimer, Carrying, Rest, Hunger, Starvation
83 + Transform,
84 + SimPos,
85 + TilePos,
86 + Path,
87 + CurrentJob,
88 + IdleTimer,
89 + Carrying,
90 + Rest,
91 + Hunger,
92 + Starvation,
93 + WorkPriorities
84 94 )]
85 95 pub struct Colonist {
86 96 pub name: String,

src/jobs/mod.rs +113 -35

@@ -75,6 +75,54 @@ const NEIGHBORS: [IVec2; 8] = [
75 75 IVec2::new(-1, -1),
76 76 ];
77 77
78 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79 +pub enum WorkType {
80 + Food,
81 + Build,
82 + Gather,
83 + Haul,
84 + Tame,
85 +}
86 +
87 +impl WorkType {
88 + pub const ALL: [WorkType; 5] = [
89 + WorkType::Food,
90 + WorkType::Build,
91 + WorkType::Gather,
92 + WorkType::Haul,
93 + WorkType::Tame,
94 + ];
95 +
96 + pub fn label(self) -> &'static str {
97 + match self {
98 + WorkType::Food => "Food",
99 + WorkType::Build => "Build",
100 + WorkType::Gather => "Gather",
101 + WorkType::Haul => "Haul",
102 + WorkType::Tame => "Tame",
103 + }
104 + }
105 +}
106 +
107 +#[derive(Component, Default, Debug, Clone, PartialEq, Eq)]
108 +pub struct WorkPriorities {
109 + disabled: Vec<WorkType>,
110 +}
111 +
112 +impl WorkPriorities {
113 + pub fn allows(&self, work: WorkType) -> bool {
114 + !self.disabled.contains(&work)
115 + }
116 +
117 + pub fn toggle(&mut self, work: WorkType) {
118 + if self.allows(work) {
119 + self.disabled.push(work);
120 + } else {
121 + self.disabled.retain(|w| *w != work);
122 + }
123 + }
124 +}
125 +
78 126 #[derive(Component, Default, Debug, Clone, PartialEq)]
79 127 pub struct CurrentJob(pub Option<Job>);
80 128
@@ -880,6 +928,7 @@ fn choose_jobs(
880 928 &Hunger,
881 929 Option<&Family>,
882 930 Has<Child>,
931 + &WorkPriorities,
883 932 ),
884 933 With<Colonist>,
885 934 >,
@@ -934,8 +983,19 @@ fn choose_jobs(
934 983 .map(|(_, _, pos)| pos.0)
935 984 .collect();
936 985 let hour = hour_of_day(*tick);
937 - for (entity, tile, mut path, mut job, mut idle, mut carrying, rest, hunger, family, child) in
938 - &mut colonists
986 + for (
987 + entity,
988 + tile,
989 + mut path,
990 + mut job,
991 + mut idle,
992 + mut carrying,
993 + rest,
994 + hunger,
995 + family,
996 + child,
997 + priorities,
998 + ) in &mut colonists
939 999 {
940 1000 let exhausted = rest.0 < EXHAUSTED_REST;
941 1001 let starving = hunger.0 < STARVING && food_to_fetch(&reservations, &food_tiles);
@@ -1006,49 +1066,67 @@ fn choose_jobs(
1006 1066 planned: &planned,
1007 1067 blueprints: &blueprints,
1008 1068 };
1009 - find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
1010 - .or_else(|| find_refuel(&build, tile.0))
1011 - .or_else(|| dark.then(|| find_lighting(&build, tile.0)).flatten())
1012 - .or_else(|| {
1013 - let cooking =
1014 - wanted(ItemKind::Meal) && raw_food_source(&build, tile.0).is_some();
1015 - cooking
1016 - .then(|| {
1017 - find_cooking(&build, tile.0).or_else(|| find_campfire(&build, tile.0))
1018 - })
1019 - .flatten()
1020 - })
1021 - .or_else(|| find_butchering(&map, &reservations, &snapshot, tile.0))
1022 - .or_else(|| find_work(&map, &reservations, tile.0, |_, k| k.is_food() && wanted(k)))
1023 - .or_else(|| {
1024 - (!designations.0.is_empty())
1025 - .then(|| {
1026 - find_work(&map, &reservations, tile.0, |site, _| {
1027 - designations.0.contains(&site)
1028 - })
1069 + let cooking = || {
1070 + (wanted(ItemKind::Meal) && raw_food_source(&build, tile.0).is_some())
1071 + .then(|| find_cooking(&build, tile.0).or_else(|| find_campfire(&build, tile.0)))
1072 + .flatten()
1073 + };
1074 + let designated = || {
1075 + (!designations.0.is_empty())
1076 + .then(|| {
1077 + find_work(&map, &reservations, tile.0, |site, _| {
1078 + designations.0.contains(&site)
1029 1079 })
1030 - .flatten()
1031 - })
1032 - .or_else(|| {
1080 + })
1081 + .flatten()
1082 + };
1083 + let candidates: [(WorkType, &dyn Fn() -> Option<Task>); 15] = [
1084 + (WorkType::Haul, &|| {
1085 + find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
1086 + }),
1087 + (WorkType::Build, &|| find_refuel(&build, tile.0)),
1088 + (WorkType::Build, &|| {
1089 + dark.then(|| find_lighting(&build, tile.0)).flatten()
1090 + }),
1091 + (WorkType::Food, &cooking),
1092 + (WorkType::Food, &|| {
1093 + find_butchering(&map, &reservations, &snapshot, tile.0)
1094 + }),
1095 + (WorkType::Food, &|| {
1096 + find_work(&map, &reservations, tile.0, |_, k| k.is_food() && wanted(k))
1097 + }),
1098 + (WorkType::Gather, &designated),
1099 + (WorkType::Food, &|| {
1033 1100 wanted(ItemKind::Vegetables)
1034 1101 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1035 1102 .flatten()
1036 - })
1037 - .or_else(|| find_bed_site(&build, adults, tile.0))
1038 - .or_else(|| find_blueprint_work(&build, tile.0))
1039 - .or_else(|| find_work(&map, &reservations, tile.0, |_, k| wanted(k)))
1040 - .or_else(|| {
1103 + }),
1104 + (WorkType::Build, &|| find_bed_site(&build, adults, tile.0)),
1105 + (WorkType::Build, &|| find_blueprint_work(&build, tile.0)),
1106 + (WorkType::Gather, &|| {
1107 + find_work(&map, &reservations, tile.0, |_, k| wanted(k))
1108 + }),
1109 + (WorkType::Food, &|| {
1041 1110 wanted(ItemKind::Meat)
1042 1111 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))
1043 1112 .flatten()
1044 - })
1045 - .or_else(|| find_sowing(&map, &zones, &reservations, tile.0))
1046 - .or_else(|| {
1113 + }),
1114 + (WorkType::Food, &|| {
1115 + find_sowing(&map, &zones, &reservations, tile.0)
1116 + }),
1117 + (WorkType::Tame, &|| {
1047 1118 (!wanted(ItemKind::Meat))
1048 1119 .then(|| find_taming(&map, &reservations, &wildlife, tile.0))
1049 1120 .flatten()
1050 - })
1051 - .or_else(|| find_haul(&map, &zones, &reservations, &snapshot, tile.0))
1121 + }),
1122 + (WorkType::Haul, &|| {
1123 + find_haul(&map, &zones, &reservations, &snapshot, tile.0)
1124 + }),
1125 + ];
1126 + candidates
1127 + .iter()
1128 + .filter(|(work, _)| priorities.allows(*work))
1129 + .find_map(|(_, find)| find())
1052 1130 };
1053 1131 if let Some(task) = task {
1054 1132 if let Task::Work { site, .. } = task

src/jobs/tests.rs +19 -0

@@ -1307,3 +1307,22 @@ fn hunters_slaughter_spare_tame_animals_first() {
1307 1307 let herd = [wild, tame(0, 10), tame(1, 11), tame(2, 12)];
1308 1308 assert_eq!(pick(&herd), tame(0, 10).0);
1309 1309 }
1310 +
1311 +#[test]
1312 +fn colonists_skip_work_types_they_are_not_assigned() {
1313 + let mut app = test_app(map_with_trees(&[UVec2::new(4, 2)]));
1314 + set_goals(&mut app, &[(ItemKind::Wood, 50)]);
1315 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
1316 + app.world_mut()
1317 + .get_mut::<WorkPriorities>(colonist)
1318 + .unwrap()
1319 + .toggle(WorkType::Gather);
1320 + run_ticks(&mut app, 60 * 10);
1321 + assert_eq!(items_of(&mut app, ItemKind::Wood), 0);
1322 + app.world_mut()
1323 + .get_mut::<WorkPriorities>(colonist)
1324 + .unwrap()
1325 + .toggle(WorkType::Gather);
1326 + run_ticks(&mut app, 60 * 15);
1327 + assert!(items_of(&mut app, ItemKind::Wood) > 0);
1328 +}

src/main.rs +2 -0

@@ -14,6 +14,7 @@ mod light;
14 14 mod mood;
15 15 mod needs;
16 16 mod noise;
17 +mod panel;
17 18 mod pathfinding;
18 19 mod paths;
19 20 mod plants;
@@ -70,6 +71,7 @@ fn main() {
70 71 family::FamilyPlugin,
71 72 designations::DesignationsPlugin,
72 73 mood::MoodPlugin,
74 + panel::PanelPlugin,
73 75 plants::PlantsPlugin,
74 76 selection::SelectionPlugin,
75 77 zones::ZonesPlugin,

src/panel.rs +235 -0

@@ -0,0 +1,235 @@
1 +use bevy::{
2 + picking::hover::Hovered,
3 + prelude::*,
4 + ui_widgets::{Activate, ActivateOnPress, Button},
5 +};
6 +
7 +use crate::{
8 + colonist::Colonist,
9 + family::Child,
10 + jobs::{CurrentJob, WorkPriorities, WorkType},
11 + mood::Mood,
12 + selection::Selection,
13 + ui::{BUTTON_ACTIVE_COLOR, BUTTON_COLOR, BUTTON_HOVER_COLOR, HideableUi},
14 +};
15 +
16 +pub struct PanelPlugin;
17 +
18 +impl Plugin for PanelPlugin {
19 + fn build(&self, app: &mut App) {
20 + app.add_systems(Startup, spawn_panel)
21 + .add_systems(
22 + Update,
23 + (rebuild_roster, show_work_toggles, style_panel_buttons),
24 + )
25 + .add_observer(on_panel_activated);
26 + }
27 +}
28 +
29 +const PANEL_COLOR: Color = Color::srgba(0.08, 0.08, 0.1, 0.8);
30 +const DISABLED_COLOR: Color = Color::srgba(0.35, 0.12, 0.12, 0.9);
31 +
32 +#[derive(Component)]
33 +struct Roster;
34 +
35 +#[derive(Component)]
36 +struct WorkRow;
37 +
38 +#[derive(Component, Clone, Copy)]
39 +struct ColonistButton(Entity);
40 +
41 +#[derive(Component, Clone, Copy)]
42 +struct WorkToggle(WorkType);
43 +
44 +fn small_button(label: String) -> impl Bundle {
45 + (
46 + Button,
47 + ActivateOnPress,
48 + Hovered::default(),
49 + Node {
50 + padding: UiRect::axes(px(5), px(2)),
51 + border_radius: BorderRadius::all(px(3)),
52 + ..default()
53 + },
54 + BackgroundColor(BUTTON_COLOR),
55 + children![(Text::new(label), TextFont::from_font_size(11.0))],
56 + )
57 +}
58 +
59 +fn spawn_panel(mut commands: Commands) {
60 + let panel = commands
61 + .spawn((
62 + HideableUi,
63 + Node {
64 + position_type: PositionType::Absolute,
65 + top: px(8),
66 + right: px(8),
67 + width: px(170),
68 + flex_direction: FlexDirection::Column,
69 + row_gap: px(4),
70 + padding: UiRect::all(px(6)),
71 + border_radius: BorderRadius::all(px(6)),
72 + ..default()
73 + },
74 + BackgroundColor(PANEL_COLOR),
75 + children![(Text::new("Colonists"), TextFont::from_font_size(12.0))],
76 + ))
77 + .id();
78 + commands.spawn((
79 + Roster,
80 + Node {
81 + flex_wrap: FlexWrap::Wrap,
82 + column_gap: px(3),
83 + row_gap: px(3),
84 + ..default()
85 + },
86 + ChildOf(panel),
87 + ));
88 + let row = commands
89 + .spawn((
90 + WorkRow,
91 + Node {
92 + flex_wrap: FlexWrap::Wrap,
93 + column_gap: px(3),
94 + row_gap: px(3),
95 + display: Display::None,
96 + ..default()
97 + },
98 + ChildOf(panel),
99 + ))
100 + .id();
101 + for work in WorkType::ALL {
102 + commands.spawn((
103 + small_button(work.label().to_string()),
104 + WorkToggle(work),
105 + ChildOf(row),
106 + ));
107 + }
108 +}
109 +
110 +pub fn roster_label(name: &str, child: bool, mood: Option<f32>) -> String {
111 + let face = match mood {
112 + Some(m) if m < 0.3 => " :(",
113 + Some(m) if m > 0.7 => " :)",
114 + _ => "",
115 + };
116 + format!("{name}{}{face}", if child { " (child)" } else { "" })
117 +}
118 +
119 +#[allow(clippy::type_complexity)]
120 +fn rebuild_roster(
121 + mut commands: Commands,
122 + roster: Single<Entity, With<Roster>>,
123 + colonists: Query<(Entity, &Colonist, Has<Child>, Option<&Mood>), With<CurrentJob>>,
124 + mut shown: Local<Vec<(Entity, String)>>,
125 +) {
126 + let mut wanted: Vec<(Entity, String)> = colonists
127 + .iter()
128 + .map(|(e, c, child, mood)| (e, roster_label(&c.name, child, mood.map(|m| m.0))))
129 + .collect();
130 + wanted.sort_by_key(|(e, _)| *e);
131 + if *shown == wanted {
132 + return;
133 + }
134 + commands.entity(*roster).despawn_related::<Children>();
135 + for (entity, label) in &wanted {
136 + commands.spawn((
137 + small_button(label.clone()),
138 + ColonistButton(*entity),
139 + ChildOf(*roster),
140 + ));
141 + }
142 + *shown = wanted;
143 +}
144 +
145 +fn show_work_toggles(
146 + selection: Res<Selection>,
147 + adults: Query<(), (With<WorkPriorities>, Without<Child>)>,
148 + mut row: Single<&mut Node, With<WorkRow>>,
149 +) {
150 + let visible = matches!(*selection, Selection::Colonist(e) if adults.contains(e));
151 + let display = if visible {
152 + Display::Flex
153 + } else {
154 + Display::None
155 + };
156 + if row.display != display {
157 + row.display = display;
158 + }
159 +}
160 +
161 +#[allow(clippy::type_complexity)]
162 +fn style_panel_buttons(
163 + selection: Res<Selection>,
164 + priorities: Query<&WorkPriorities>,
165 + mut buttons: Query<(
166 + &Hovered,
167 + Option<&WorkToggle>,
168 + Option<&ColonistButton>,
169 + &mut BackgroundColor,
170 + )>,
171 +) {
172 + let selected = match *selection {
173 + Selection::Colonist(e) => Some(e),
174 + _ => None,
175 + };
176 + for (hovered, toggle, colonist, mut color) in &mut buttons {
177 + let new = match (toggle, colonist) {
178 + (Some(toggle), _) => {
179 + let allowed = selected
180 + .and_then(|e| priorities.get(e).ok())
181 + .is_none_or(|p| p.allows(toggle.0));
182 + if allowed {
183 + BUTTON_ACTIVE_COLOR
184 + } else {
185 + DISABLED_COLOR
186 + }
187 + }
188 + (None, Some(c)) if Some(c.0) == selected => BUTTON_ACTIVE_COLOR,
189 + (None, Some(_)) if hovered.get() => BUTTON_HOVER_COLOR,
190 + (None, Some(_)) => BUTTON_COLOR,
191 + (None, None) => continue,
192 + };
193 + color.set_if_neq(BackgroundColor(new));
194 + }
195 +}
196 +
197 +fn on_panel_activated(
198 + activate: On<Activate>,
199 + toggles: Query<&WorkToggle>,
200 + colonist_buttons: Query<&ColonistButton>,
201 + mut selection: ResMut<Selection>,
202 + mut priorities: Query<&mut WorkPriorities>,
203 +) {
204 + if let Ok(button) = colonist_buttons.get(activate.entity) {
205 + *selection = Selection::Colonist(button.0);
206 + } else if let Ok(toggle) = toggles.get(activate.entity)
207 + && let Selection::Colonist(entity) = *selection
208 + && let Ok(mut work) = priorities.get_mut(entity)
209 + {
210 + work.toggle(toggle.0);
211 + }
212 +}
213 +
214 +#[cfg(test)]
215 +mod tests {
216 + use super::*;
217 +
218 + #[test]
219 + fn roster_labels_show_children_and_strong_moods() {
220 + assert_eq!(roster_label("Ada", false, Some(0.5)), "Ada");
221 + assert_eq!(roster_label("Ivo", true, Some(0.8)), "Ivo (child) :)");
222 + assert_eq!(roster_label("Bram", false, Some(0.1)), "Bram :(");
223 + }
224 +
225 + #[test]
226 + fn work_types_can_be_switched_off_and_on() {
227 + let mut work = WorkPriorities::default();
228 + assert!(work.allows(WorkType::Haul));
229 + work.toggle(WorkType::Haul);
230 + assert!(!work.allows(WorkType::Haul));
231 + assert!(work.allows(WorkType::Food));
232 + work.toggle(WorkType::Haul);
233 + assert!(work.allows(WorkType::Haul));
234 + }
235 +}

src/selection.rs +1 -0

@@ -236,6 +236,7 @@ fn spawn_tile_info(mut commands: Commands) {
236 236 position_type: PositionType::Absolute,
237 237 top: px(8),
238 238 left: px(8),
239 + max_width: percent(70),
239 240 padding: UiRect::all(px(6)),
240 241 ..default()
241 242 },

src/ui.rs +3 -3

@@ -53,9 +53,9 @@ const TOOLBAR: [Action; 14] = [
53 53 Action::ToggleUi,
54 54 ];
55 55
56 -const BUTTON_COLOR: Color = Color::srgba(0.12, 0.12, 0.14, 0.9);
57 -const BUTTON_HOVER_COLOR: Color = Color::srgba(0.22, 0.22, 0.26, 0.95);
58 -const BUTTON_ACTIVE_COLOR: Color = Color::srgba(0.2, 0.45, 0.75, 0.95);
56 +pub const BUTTON_COLOR: Color = Color::srgba(0.12, 0.12, 0.14, 0.9);
57 +pub const BUTTON_HOVER_COLOR: Color = Color::srgba(0.22, 0.22, 0.26, 0.95);
58 +pub const BUTTON_ACTIVE_COLOR: Color = Color::srgba(0.2, 0.45, 0.75, 0.95);
59 59 const BUTTON_PRESSED_COLOR: Color = Color::srgba(0.3, 0.6, 0.9, 1.0);
60 60 const KEY_HINT_COLOR: Color = Color::srgb(0.65, 0.65, 0.7);
61 61

tasks.md +1 -2

@@ -32,12 +32,11 @@
32 32 - [x] Walls cast shadows, roofs keep daylight out; deconstruction (build tool mode, refunds half)
33 33 - [x] Animals flee, ranged hunting, carcasses butchered into meat, taming with breeding herds and slaughter of spare tame animals
34 34 - [x] Mood (needs, meals, partner, home, sickness, grief), food poisoning from raw food, unhappy colonists leave, ageing and death of old age
35 +- [x] Colonist panel: roster with mood faces (click to select) and per-colonist work type toggles
35 36 - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map
36 37
37 38 ## Planned
38 -- [ ] Per-colonist work priorities (busy colonies leave low-priority work such as hauling undone)
39 39 - [ ] Stockpile filters (which item kinds a zone accepts) and storage priority
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)
43 -- [ ] Colonist panel / better info UI (info panel is plain text)