Group the roster by household in a scrollable list
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-10-25 18:36 UTC
- commit
- e47049d32a2517681255bb24641215519b0a0594
- parent
- b092f10569
- tree
- browse at this commit
4 files changed +213 -15
| CLAUDE.md | +1 -1 | |
| src/camera.rs | +2 -1 | |
| src/panel.rs | +209 -13 | |
| tasks.md | +1 -0 |
CLAUDE.md +1 -1
| @@ -45,7 +45,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che | ||
| 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): homes of extinct families move to `Homes.vacant` (serde default; `all()`, `is_home()` cover owned + vacant; `owner()` owned only, so vacant beds count as communal for sleeping); `abandoned_home` adopts enclosed HOME_INTERIOR rooms around unowned beds as vacant (old saves); couples without a home take the vacant home nearest the colony before a new one is planned. Floods `reachable` from colony home (if walkable) and drops blueprints with no reachable tile in their 3×3 (sealed pockets inflated goals forever). Per home: `door_repair` (no door/planned door whose `outward` tile is reachable → Door on an empty door site, else Deconstruct the best wall; runs even with blueprints pending), else roofs (`roofs_needed`), hearth at `Home::hearth_tile` (hi−1), statue + `BuildKind::Floor` on unfloored interior tiles (`floored`; doesn't overwrite other blueprints) for owned built homes. `surplus_beds`: reachable communal beds beyond homeless adults → Deconstruct (nearest kept). Households (`Household{partners, children}` per family; `adults()` = max(partners,1)) size homes via `desired_interior` (solo 3×2, couple 4×3, +1 child 5×3, 2+ children 5×4). Couples get a vacant home (preferring ones big enough) or `plan_sized`; otherwise one solo adult per pass (no Partner, not Child, family None / parents' family / no home) gets a new `Family` (Commands) and a vacant home ≤ 4×3 or a planned solo room. Owned built homes smaller than desired get `HomeContext::expansion`: grow one side (width first, then height) where the new ring may reuse walls and the new strip is free; opened old wall/door tiles get Deconstruct, missing new ring tiles Wall; Homes entry updated at once. Owned built homes get Bed blueprints up to `adults()` at `bed_tiles_for`. New homes via `HomeContext::plan_sized`: BFS from colony home (center ≥ HOME_MIN_DISTANCE), first HOME_CANDIDATES fits, pick the one sharing most walls (`shared_walls`: ring tiles may be existing walls or planned wall blueprints, else open and not in a room; interior open and not in a room; some door site must open onto walkable, non-room ground). `Home::blueprints(map, toward)` plans only missing ring walls, door via `door_site` (non-wall, walkable outward), beds. Adoption: any rectangular room (`home_of_room`, interior ≥ MIN_HOME_INTERIOR 3×2) with ≥2 beds not already a home → vacant. `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 (owned or vacant). 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 | 47 | - Storage rules (zones.rs): `Zones.rules` per stockpile tile `StorageRule{refused: Vec<Stock>, priority: StoragePriority::{Low, Normal, High}}` (absent = default), `accepts(tile, kind)`, `update_region_rule` applies to the 4-connected stockpile region; removing tiles drops rules. `find_storage` tries priorities High→Normal→Low, nearest accepting free tile with room; `find_haul` treats items as loose when their tile doesn't accept them or a higher-priority accepting tile with room exists (`storage_candidates`), so stock moves up the priorities and settles. `ColonyGoals` overrides (`adjust` ±, `reset`, `is_overridden`) win over learned goals. |
| 48 | -- `src/panel.rs` top-right colonist panel (also: stockpile section with accept toggles + priority cycle when a stockpile tile is selected; collapsible Stock targets with -/+/auto per Stock): 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. | |
| 48 | +- `src/panel.rs` top-right colonist panel. Roster = scrollable column (max ROSTER_MAX_HEIGHT, `Overflow::scroll_y`, `scroll_roster` moves `ScrollPosition` while `Hovered`; camera `scroll_zoom` ignores the wheel over UI) of `HouseholdButton` headers from `household_rows` (grouped by Family, None → "Others"; label = adult names, count, " :(" / " hurt" flags) that toggle `ExpandedHouseholds`; expanded rows list `ColonistButton`s; selecting a colonist expands their household. (Also: stockpile section with accept toggles + priority cycle when a stockpile tile is selected; collapsible Stock targets with -/+/auto per Stock): 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. | |
| 49 | 49 | - `src/climate.rs` seasons/temperature. Year = YEAR_DAYS 12 (4 seasons × SEASON_DAYS 3, starts Spring; independent of ageing's day=year). `outdoor_temperature(tick)` = seasonal cosine (MEAN 12, swing 14, coldest at COLDEST_YEAR_FRACTION) + day/night swing (coldest 04:00); `daily_mean(day)`, `growing_day` (mean ≥ 8), `sowing_season` (next SOW_AHEAD_DAYS growing; gates find_sowing), `cold_days_ahead` (non-growing days in the next FORESIGHT_DAYS 6 → `ColonyGoals::set_cold_days`, food/wood buffer days = max(normal, cold days)). `growth_factor(temp)` scales plant growth; Crop with growth < 1 dies below FROST_TEMP. `spoil_factor` slows spoilage in cold. `tile_temperature(map, outdoor, tile, fires)`: roof pulls toward 16 by ROOF_INSULATION 0.7, fires add `StructureKind::heat()` with light-radius falloff, blocked by walls. `Warmth(f32)` on colonists (required), `update_warmth` every 60 ticks; `cold()` < COLD_TEMP 5 → hunger ×1.5, rest recovery ×0.5, mood −0.15. choose_jobs: cold colonists MoveTo `find_warm_spot` (tiles ≥ WARM_TEMP near fires) before work; `chilly_soon` triggers find_lighting's campfire (torches only when dark). `snow_cover(tick)` → `LightMap::with_snow` (8 steps, triggers recompute) → tilemap `Layer::Snow` (z 0.2, white alpha overlay on unroofed land). Info panel shows season/day/temp (font has no ° glyph). |
| 50 | 50 | - Crafting (jobs): `Task::Craft{recipe: Recipe::{Meal, Tools}, pickup, station, progress}` (was Cook): recipe gives station kind (Campfire/Workbench), accepted input, input count, work, output; `find_crafting`, `input_source`. Tools: `WorkType::Craft` candidate when Tools wanted and TOOL_IRON iron available → craft at a workbench, else `find_workbench` (one, near home, autonomous Structure build like campfire/bed). `Task::Equip{pickup}` (before autonomous candidates) for adults without `Tool{wear}`; equip records Tools use (learned demand); `tool_rate` ×1.5 work; wear per working tick, removed at TOOL_LIFE_TICKS (2 days). Iron has min goal 6. `StructureKind::Statue` (5 gold): plan_homes adds one blueprint at `Home::statue_tile` once a home is built → gold demand via planned_needs; mood +0.1 (`beauty`) when the family home has one; +0.05 (`floored`) when its interior is all WoodFloor. |
| 51 | 51 | - `src/save.rs` save/load. Action::Save/Load (F5/F9). `snapshot(world) -> SaveGame` (serde_json; SAVE_VERSION): seed, tick, rng, `WorldMap::layers()` (`MapLayers`: RLE terrain/plants/walls/roofs as u8 + sparse growth/wear; heights regenerated by `generate(size, seed)` in `restore_map`), `Zones::save` (`ZonesSave`, UVec2-keyed maps become Vecs — serde_json needs string keys), blueprints Vec, Homes, Designations, ColonyGoals, log, losses, colonists (`ColonistSave`, partner as index), items, structures (+fuel), animals. Not saved: jobs, paths, reservations, Affinity, idle timers (colonists restart idle). Storage: web localStorage key `simulator-save` (~150 KB), native `simulator-save.json` in cwd. Load: `start_load` (PreUpdate, exclusive) parses, inserts WorldSeed + restored map, writes WorldGenerated (all reset systems run in Update), stores `PendingLoad`; `finish_load` (PostUpdate) despawns colonists/animals/items/structures and `apply`s the save via SystemState spawn helpers (`spawn_colonist`, `spawn_stack`, `spawn_structure` → Entity, `spawn_animal`). New persistent state = add to SaveGame + snapshot + apply (+ serde derives; bevy `serialize` feature for glam). |
src/camera.rs +2 -1
| @@ -152,6 +152,7 @@ fn drag_pan( | ||
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | fn scroll_zoom( |
| 155 | + over_ui: Res<crate::ui::PointerOverUi>, | |
| 155 | 156 | scroll: Res<AccumulatedMouseScroll>, |
| 156 | 157 | pixels_per_line: Res<MouseScrollPixelsPerLine>, |
| 157 | 158 | window: Single<&Window, With<PrimaryWindow>>, |
| @@ -164,7 +165,7 @@ fn scroll_zoom( | ||
| 164 | 165 | )>, |
| 165 | 166 | ) { |
| 166 | 167 | let lines = scroll.to_lines(&pixels_per_line).delta.y; |
| 167 | - if lines == 0.0 { | |
| 168 | + if lines == 0.0 || over_ui.0 { | |
| 168 | 169 | return; |
| 169 | 170 | } |
| 170 | 171 | let (camera, global, mut transform, mut projection, controller) = camera.into_inner(); |
src/panel.rs +209 -13
| @@ -7,10 +7,11 @@ use bevy::{ | ||
| 7 | 7 | use crate::{ |
| 8 | 8 | colonist::Colonist, |
| 9 | 9 | economy::{ColonyGoals, Stock}, |
| 10 | - family::Child, | |
| 10 | + family::{Child, Family, Partner}, | |
| 11 | 11 | jobs::{CurrentJob, WorkPriorities, WorkType}, |
| 12 | 12 | mood::Mood, |
| 13 | 13 | selection::Selection, |
| 14 | + threats::{HURT, Health}, | |
| 14 | 15 | ui::{BUTTON_ACTIVE_COLOR, BUTTON_COLOR, BUTTON_HOVER_COLOR, HideableUi}, |
| 15 | 16 | zones::Zones, |
| 16 | 17 | }; |
| @@ -23,13 +24,16 @@ impl Plugin for PanelPlugin { | ||
| 23 | 24 | .add_systems( |
| 24 | 25 | Update, |
| 25 | 26 | ( |
| 27 | + expand_selected_household, | |
| 26 | 28 | rebuild_roster, |
| 29 | + scroll_roster, | |
| 27 | 30 | show_work_toggles, |
| 28 | 31 | style_panel_buttons, |
| 29 | 32 | show_storage, |
| 30 | 33 | show_targets, |
| 31 | 34 | ), |
| 32 | 35 | ) |
| 36 | + .init_resource::<ExpandedHouseholds>() | |
| 33 | 37 | .add_observer(on_panel_activated); |
| 34 | 38 | } |
| 35 | 39 | } |
| @@ -47,6 +51,15 @@ struct WorkRow; | ||
| 47 | 51 | struct ColonistButton(Entity); |
| 48 | 52 | |
| 49 | 53 | #[derive(Component, Clone, Copy)] |
| 54 | +struct HouseholdButton(Option<u32>); | |
| 55 | + | |
| 56 | +#[derive(Resource, Default, Debug)] | |
| 57 | +struct ExpandedHouseholds(std::collections::HashSet<Option<u32>>); | |
| 58 | + | |
| 59 | +const ROSTER_MAX_HEIGHT: f32 = 320.0; | |
| 60 | +const SCROLL_LINE: f32 = 20.0; | |
| 61 | + | |
| 62 | +#[derive(Component, Clone, Copy)] | |
| 50 | 63 | struct WorkToggle(WorkType); |
| 51 | 64 | |
| 52 | 65 | #[derive(Component)] |
| @@ -124,10 +137,13 @@ fn spawn_panel(mut commands: Commands) { | ||
| 124 | 137 | .id(); |
| 125 | 138 | commands.spawn(( |
| 126 | 139 | Roster, |
| 140 | + Hovered::default(), | |
| 141 | + ScrollPosition::default(), | |
| 127 | 142 | Node { |
| 128 | - flex_wrap: FlexWrap::Wrap, | |
| 129 | - column_gap: px(3), | |
| 143 | + flex_direction: FlexDirection::Column, | |
| 130 | 144 | row_gap: px(3), |
| 145 | + max_height: px(ROSTER_MAX_HEIGHT), | |
| 146 | + overflow: Overflow::scroll_y(), | |
| 131 | 147 | ..default() |
| 132 | 148 | }, |
| 133 | 149 | ChildOf(panel), |
| @@ -293,32 +309,177 @@ pub fn roster_label(name: &str, child: bool, mood: Option<f32>) -> String { | ||
| 293 | 309 | format!("{name}{}{face}", if child { " (child)" } else { "" }) |
| 294 | 310 | } |
| 295 | 311 | |
| 312 | +#[derive(Debug, Clone, PartialEq)] | |
| 313 | +pub struct Member { | |
| 314 | + pub entity: Entity, | |
| 315 | + pub name: String, | |
| 316 | + pub child: bool, | |
| 317 | + pub partner: bool, | |
| 318 | + pub mood: Option<f32>, | |
| 319 | + pub hurt: bool, | |
| 320 | +} | |
| 321 | + | |
| 322 | +#[derive(Debug, Clone, PartialEq)] | |
| 323 | +pub struct HouseholdRow { | |
| 324 | + pub family: Option<u32>, | |
| 325 | + pub label: String, | |
| 326 | + pub members: Vec<(Entity, String)>, | |
| 327 | +} | |
| 328 | + | |
| 329 | +pub fn household_rows(members: &[(Option<u32>, Member)]) -> Vec<HouseholdRow> { | |
| 330 | + let mut groups: Vec<(Option<u32>, Vec<&Member>)> = Vec::new(); | |
| 331 | + for (family, member) in members { | |
| 332 | + match groups.iter_mut().find(|(f, _)| f == family) { | |
| 333 | + Some((_, list)) => list.push(member), | |
| 334 | + None => groups.push((*family, vec![member])), | |
| 335 | + } | |
| 336 | + } | |
| 337 | + let mut rows: Vec<HouseholdRow> = groups | |
| 338 | + .into_iter() | |
| 339 | + .map(|(family, mut list)| { | |
| 340 | + list.sort_by_key(|m| (m.child, !m.partner, m.name.clone())); | |
| 341 | + let heads: Vec<&str> = list | |
| 342 | + .iter() | |
| 343 | + .filter(|m| !m.child) | |
| 344 | + .take(2) | |
| 345 | + .map(|m| m.name.as_str()) | |
| 346 | + .collect(); | |
| 347 | + let title = match (family, heads.as_slice()) { | |
| 348 | + (None, _) => "Others".to_string(), | |
| 349 | + (_, []) => list[0].name.clone(), | |
| 350 | + (_, names) => names.join(" & "), | |
| 351 | + }; | |
| 352 | + let sad = list.iter().any(|m| m.mood.is_some_and(|v| v < 0.3)); | |
| 353 | + let hurt = list.iter().any(|m| m.hurt); | |
| 354 | + let label = format!( | |
| 355 | + "{title} ({}){}{}", | |
| 356 | + list.len(), | |
| 357 | + if sad { " :(" } else { "" }, | |
| 358 | + if hurt { " hurt" } else { "" } | |
| 359 | + ); | |
| 360 | + let members = list | |
| 361 | + .iter() | |
| 362 | + .map(|m| { | |
| 363 | + let label = roster_label(&m.name, m.child, m.mood); | |
| 364 | + (m.entity, if m.hurt { label + " hurt" } else { label }) | |
| 365 | + }) | |
| 366 | + .collect(); | |
| 367 | + HouseholdRow { | |
| 368 | + family, | |
| 369 | + label, | |
| 370 | + members, | |
| 371 | + } | |
| 372 | + }) | |
| 373 | + .collect(); | |
| 374 | + rows.sort_by(|a, b| (a.family.is_none(), &a.label).cmp(&(b.family.is_none(), &b.label))); | |
| 375 | + rows | |
| 376 | +} | |
| 377 | + | |
| 296 | 378 | #[allow(clippy::type_complexity)] |
| 297 | 379 | fn rebuild_roster( |
| 298 | 380 | mut commands: Commands, |
| 299 | 381 | roster: Single<Entity, With<Roster>>, |
| 300 | - colonists: Query<(Entity, &Colonist, Has<Child>, Option<&Mood>), With<CurrentJob>>, | |
| 301 | - mut shown: Local<Vec<(Entity, String)>>, | |
| 382 | + expanded: Res<ExpandedHouseholds>, | |
| 383 | + colonists: Query< | |
| 384 | + ( | |
| 385 | + Entity, | |
| 386 | + &Colonist, | |
| 387 | + Option<&Family>, | |
| 388 | + Has<Child>, | |
| 389 | + Has<Partner>, | |
| 390 | + Option<&Mood>, | |
| 391 | + Option<&Health>, | |
| 392 | + ), | |
| 393 | + With<CurrentJob>, | |
| 394 | + >, | |
| 395 | + mut shown: Local<Vec<(HouseholdRow, bool)>>, | |
| 302 | 396 | ) { |
| 303 | - let mut wanted: Vec<(Entity, String)> = colonists | |
| 397 | + let members: Vec<(Option<u32>, Member)> = colonists | |
| 304 | 398 | .iter() |
| 305 | - .map(|(e, c, child, mood)| (e, roster_label(&c.name, child, mood.map(|m| m.0)))) | |
| 399 | + .map(|(entity, c, family, child, partner, mood, health)| { | |
| 400 | + let member = Member { | |
| 401 | + entity, | |
| 402 | + name: c.name.clone(), | |
| 403 | + child, | |
| 404 | + partner, | |
| 405 | + mood: mood.map(|m| m.0), | |
| 406 | + hurt: health.is_some_and(|h| h.0 < HURT), | |
| 407 | + }; | |
| 408 | + (family.map(|f| f.0), member) | |
| 409 | + }) | |
| 410 | + .collect(); | |
| 411 | + let wanted: Vec<(HouseholdRow, bool)> = household_rows(&members) | |
| 412 | + .into_iter() | |
| 413 | + .map(|row| { | |
| 414 | + let open = expanded.0.contains(&row.family); | |
| 415 | + (row, open) | |
| 416 | + }) | |
| 306 | 417 | .collect(); |
| 307 | - wanted.sort_by_key(|(e, _)| *e); | |
| 308 | 418 | if *shown == wanted { |
| 309 | 419 | return; |
| 310 | 420 | } |
| 311 | 421 | commands.entity(*roster).despawn_related::<Children>(); |
| 312 | - for (entity, label) in &wanted { | |
| 422 | + for (row, open) in &wanted { | |
| 423 | + let marker = if *open { "-" } else { "+" }; | |
| 313 | 424 | commands.spawn(( |
| 314 | - small_button(label.clone()), | |
| 315 | - ColonistButton(*entity), | |
| 425 | + small_button(format!("{marker} {}", row.label)), | |
| 426 | + HouseholdButton(row.family), | |
| 316 | 427 | ChildOf(*roster), |
| 317 | 428 | )); |
| 429 | + if !open { | |
| 430 | + continue; | |
| 431 | + } | |
| 432 | + let list = commands | |
| 433 | + .spawn(( | |
| 434 | + Node { | |
| 435 | + margin: UiRect::left(px(8)), | |
| 436 | + ..wrap_row() | |
| 437 | + }, | |
| 438 | + ChildOf(*roster), | |
| 439 | + )) | |
| 440 | + .id(); | |
| 441 | + for (entity, label) in &row.members { | |
| 442 | + commands.spawn(( | |
| 443 | + small_button(label.clone()), | |
| 444 | + ColonistButton(*entity), | |
| 445 | + ChildOf(list), | |
| 446 | + )); | |
| 447 | + } | |
| 318 | 448 | } |
| 319 | 449 | *shown = wanted; |
| 320 | 450 | } |
| 321 | 451 | |
| 452 | +fn expand_selected_household( | |
| 453 | + selection: Res<Selection>, | |
| 454 | + families: Query<Option<&Family>, With<Colonist>>, | |
| 455 | + mut expanded: ResMut<ExpandedHouseholds>, | |
| 456 | +) { | |
| 457 | + if !selection.is_changed() { | |
| 458 | + return; | |
| 459 | + } | |
| 460 | + if let Selection::Colonist(entity) = *selection | |
| 461 | + && let Ok(family) = families.get(entity) | |
| 462 | + && !expanded.0.contains(&family.map(|f| f.0)) | |
| 463 | + { | |
| 464 | + expanded.0.insert(family.map(|f| f.0)); | |
| 465 | + } | |
| 466 | +} | |
| 467 | + | |
| 468 | +fn scroll_roster( | |
| 469 | + scroll: Res<bevy::input::mouse::AccumulatedMouseScroll>, | |
| 470 | + pixels_per_line: Res<bevy::input::mouse::MouseScrollPixelsPerLine>, | |
| 471 | + roster: Single<(&Hovered, &mut ScrollPosition, &ComputedNode), With<Roster>>, | |
| 472 | +) { | |
| 473 | + let (hovered, mut position, computed) = roster.into_inner(); | |
| 474 | + let lines = scroll.to_lines(&pixels_per_line).delta.y; | |
| 475 | + if lines == 0.0 || !hovered.get() { | |
| 476 | + return; | |
| 477 | + } | |
| 478 | + let max = ((computed.content_size().y - computed.size().y) * computed.inverse_scale_factor()) | |
| 479 | + .max(0.0); | |
| 480 | + position.y = (position.y - lines * SCROLL_LINE).clamp(0.0, max); | |
| 481 | +} | |
| 482 | + | |
| 322 | 483 | fn show_work_toggles( |
| 323 | 484 | selection: Res<Selection>, |
| 324 | 485 | adults: Query<(), (With<WorkPriorities>, Without<Child>)>, |
| @@ -343,6 +504,7 @@ fn style_panel_buttons( | ||
| 343 | 504 | &Hovered, |
| 344 | 505 | Option<&WorkToggle>, |
| 345 | 506 | Option<&ColonistButton>, |
| 507 | + Has<HouseholdButton>, | |
| 346 | 508 | &mut BackgroundColor, |
| 347 | 509 | )>, |
| 348 | 510 | ) { |
| @@ -350,7 +512,7 @@ fn style_panel_buttons( | ||
| 350 | 512 | Selection::Colonist(e) => Some(e), |
| 351 | 513 | _ => None, |
| 352 | 514 | }; |
| 353 | - for (hovered, toggle, colonist, mut color) in &mut buttons { | |
| 515 | + for (hovered, toggle, colonist, household, mut color) in &mut buttons { | |
| 354 | 516 | let new = match (toggle, colonist) { |
| 355 | 517 | (Some(toggle), _) => { |
| 356 | 518 | let allowed = selected |
| @@ -365,6 +527,8 @@ fn style_panel_buttons( | ||
| 365 | 527 | (None, Some(c)) if Some(c.0) == selected => BUTTON_ACTIVE_COLOR, |
| 366 | 528 | (None, Some(_)) if hovered.get() => BUTTON_HOVER_COLOR, |
| 367 | 529 | (None, Some(_)) => BUTTON_COLOR, |
| 530 | + (None, None) if household && hovered.get() => BUTTON_HOVER_COLOR, | |
| 531 | + (None, None) if household => BUTTON_COLOR, | |
| 368 | 532 | (None, None) => continue, |
| 369 | 533 | }; |
| 370 | 534 | color.set_if_neq(BackgroundColor(new)); |
| @@ -384,8 +548,14 @@ fn on_panel_activated( | ||
| 384 | 548 | mut zones: ResMut<Zones>, |
| 385 | 549 | mut goals: ResMut<ColonyGoals>, |
| 386 | 550 | mut priorities: Query<&mut WorkPriorities>, |
| 551 | + households: (Query<&HouseholdButton>, ResMut<ExpandedHouseholds>), | |
| 387 | 552 | ) { |
| 388 | - if targets_toggle.contains(activate.entity) { | |
| 553 | + let (household_buttons, mut expanded) = households; | |
| 554 | + if let Ok(button) = household_buttons.get(activate.entity) { | |
| 555 | + if !expanded.0.remove(&button.0) { | |
| 556 | + expanded.0.insert(button.0); | |
| 557 | + } | |
| 558 | + } else if targets_toggle.contains(activate.entity) { | |
| 389 | 559 | targets.display = match targets.display { |
| 390 | 560 | Display::None => Display::Flex, |
| 391 | 561 | _ => Display::None, |
| @@ -434,4 +604,30 @@ mod tests { | ||
| 434 | 604 | work.toggle(WorkType::Haul); |
| 435 | 605 | assert!(work.allows(WorkType::Haul)); |
| 436 | 606 | } |
| 607 | + | |
| 608 | + // Big colonies don't fit as a flat list of names: the roster groups colonists by household | |
| 609 | + // (partners first, then children), names each household after its adults, and flags any | |
| 610 | + // household with an unhappy or hurt member so it stands out while collapsed. | |
| 611 | + #[test] | |
| 612 | + fn roster_groups_colonists_by_household() { | |
| 613 | + let e = |i: u32| Entity::from_raw_u32(i).unwrap(); | |
| 614 | + let member = |i, name: &str, child, partner, mood, hurt| Member { | |
| 615 | + entity: e(i), | |
| 616 | + name: name.into(), | |
| 617 | + child, | |
| 618 | + partner, | |
| 619 | + mood: Some(mood), | |
| 620 | + hurt, | |
| 621 | + }; | |
| 622 | + let rows = household_rows(&[ | |
| 623 | + (Some(1), member(1, "Bram", false, true, 0.5, false)), | |
| 624 | + (Some(1), member(2, "Ivo", true, false, 0.2, false)), | |
| 625 | + (Some(1), member(3, "Ada", false, true, 0.5, true)), | |
| 626 | + (None, member(4, "Zeno", false, false, 0.5, false)), | |
| 627 | + (Some(2), member(5, "Cleo", false, false, 0.8, false)), | |
| 628 | + ]); | |
| 629 | + let labels: Vec<&str> = rows.iter().map(|r| r.label.as_str()).collect(); | |
| 630 | + assert_eq!(labels, ["Ada & Bram (3) :( hurt", "Cleo (1)", "Others (1)"]); | |
| 631 | + assert_eq!(rows[0].members[2].1, "Ivo (child) :("); | |
| 632 | + } | |
| 437 | 633 | } |
tasks.md +1 -0
| @@ -57,6 +57,7 @@ | ||
| 57 | 57 | - [x] Colonists keep busy paths (paved or heavily worn) free when planning buildings |
| 58 | 58 | - [x] Optimization: wolves give up on unreachable prey, cached warm spots, cheaper heat checks |
| 59 | 59 | - [x] Bridges: colonists bridge busy wading spots and water on much shorter (or otherwise impossible) routes, over deep water from the shore outwards; Build tool Bridge mode |
| 60 | +- [x] Colonist panel for big colonies: households as collapsible, flagged rows in a scrollable list | |
| 60 | 61 | - [x] Action/keybind system, toolbar buttons for every keybind, hide-UI toggle (H), clicks on UI don't reach the map |
| 61 | 62 | |
| 62 | 63 | ## Planned |