use bevy::{ picking::hover::Hovered, prelude::*, ui_widgets::{Activate, ActivateOnPress, Button}, }; use crate::{ colonist::Colonist, economy::{ColonyGoals, Stock}, family::Child, jobs::{CurrentJob, WorkPriorities, WorkType}, mood::Mood, selection::Selection, ui::{BUTTON_ACTIVE_COLOR, BUTTON_COLOR, BUTTON_HOVER_COLOR, HideableUi}, zones::Zones, }; pub struct PanelPlugin; impl Plugin for PanelPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_panel) .add_systems( Update, ( rebuild_roster, show_work_toggles, style_panel_buttons, show_storage, show_targets, ), ) .add_observer(on_panel_activated); } } const PANEL_COLOR: Color = Color::srgba(0.08, 0.08, 0.1, 0.8); const DISABLED_COLOR: Color = Color::srgba(0.35, 0.12, 0.12, 0.9); #[derive(Component)] struct Roster; #[derive(Component)] struct WorkRow; #[derive(Component, Clone, Copy)] struct ColonistButton(Entity); #[derive(Component, Clone, Copy)] struct WorkToggle(WorkType); #[derive(Component)] struct StorageSection; #[derive(Component, Clone, Copy)] struct StorageToggle(Stock); #[derive(Component)] struct PriorityButton; #[derive(Component)] struct PriorityLabel; #[derive(Component)] struct TargetsToggle; #[derive(Component)] struct TargetsSection; #[derive(Component, Clone, Copy)] struct GoalText(Stock); #[derive(Component, Clone, Copy)] struct GoalButton { stock: Stock, delta: i32, } const GOAL_STEP: i32 = 10; fn wrap_row() -> Node { Node { flex_wrap: FlexWrap::Wrap, column_gap: px(3), row_gap: px(3), align_items: AlignItems::Center, ..default() } } fn small_button(label: String) -> impl Bundle { ( Button, ActivateOnPress, Hovered::default(), Node { padding: UiRect::axes(px(5), px(2)), border_radius: BorderRadius::all(px(3)), ..default() }, BackgroundColor(BUTTON_COLOR), children![(Text::new(label), TextFont::from_font_size(11.0))], ) } fn spawn_panel(mut commands: Commands) { let panel = commands .spawn(( HideableUi, Node { position_type: PositionType::Absolute, top: px(8), right: px(8), width: px(170), flex_direction: FlexDirection::Column, row_gap: px(4), padding: UiRect::all(px(6)), border_radius: BorderRadius::all(px(6)), ..default() }, BackgroundColor(PANEL_COLOR), children![(Text::new("Colonists"), TextFont::from_font_size(12.0))], )) .id(); commands.spawn(( Roster, Node { flex_wrap: FlexWrap::Wrap, column_gap: px(3), row_gap: px(3), ..default() }, ChildOf(panel), )); let row = commands .spawn(( WorkRow, Node { flex_wrap: FlexWrap::Wrap, column_gap: px(3), row_gap: px(3), display: Display::None, ..default() }, ChildOf(panel), )) .id(); for work in WorkType::ALL { commands.spawn(( small_button(work.label().to_string()), WorkToggle(work), ChildOf(row), )); } let storage = commands .spawn(( StorageSection, Node { display: Display::None, ..wrap_row() }, ChildOf(panel), children![( Text::new("Stockpile accepts:"), TextFont::from_font_size(11.0) )], )) .id(); for stock in Stock::ALL { commands.spawn(( small_button(stock.label().to_string()), StorageToggle(stock), ChildOf(storage), )); } let priority = commands .spawn(( small_button(String::new()), PriorityButton, ChildOf(storage), )) .id(); commands.spawn(( PriorityLabel, Text::new("Priority"), TextFont::from_font_size(11.0), ChildOf(priority), )); commands.spawn(( small_button("Stock targets".into()), TargetsToggle, ChildOf(panel), )); let targets = commands .spawn(( TargetsSection, Node { display: Display::None, flex_direction: FlexDirection::Column, row_gap: px(2), ..default() }, ChildOf(panel), )) .id(); for stock in Stock::ALL { let line = commands.spawn((wrap_row(), ChildOf(targets))).id(); commands.spawn(( GoalText(stock), Text::new(stock.label()), TextFont::from_font_size(11.0), Node { width: px(78), ..default() }, ChildOf(line), )); for (label, delta) in [("-", -GOAL_STEP), ("+", GOAL_STEP), ("auto", 0)] { commands.spawn(( small_button(label.into()), GoalButton { stock, delta }, ChildOf(line), )); } } } fn selected_stockpile(selection: &Selection, zones: &Zones) -> Option { match *selection { Selection::Tile(tile) if zones.is_stockpile(tile) => Some(tile), _ => None, } } fn show_storage( selection: Res, zones: Res, mut section: Single<&mut Node, With>, mut toggles: Query<(&StorageToggle, &mut BackgroundColor)>, mut label: Single<&mut Text, With>, ) { let tile = selected_stockpile(&selection, &zones); let display = if tile.is_some() { Display::Flex } else { Display::None }; if section.display != display { section.display = display; } let Some(tile) = tile else { return; }; let rule = zones.rule(tile); for (toggle, mut color) in &mut toggles { let accepted = !rule.refused.contains(&toggle.0); let new = if accepted { BUTTON_ACTIVE_COLOR } else { DISABLED_COLOR }; color.set_if_neq(BackgroundColor(new)); } let text = format!("Priority: {}", rule.priority.label()); if label.0 != text { label.0 = text; } } fn show_targets( goals: Res, section: Single<&Node, With>, mut texts: Query<(&GoalText, &mut Text)>, ) { if section.display == Display::None { return; } for (goal, mut text) in &mut texts { let marker = if goals.is_overridden(goal.0) { "*" } else { "" }; let line = format!("{} {}{marker}", goal.0.label(), goals.goal(goal.0)); if text.0 != line { text.0 = line; } } } pub fn roster_label(name: &str, child: bool, mood: Option) -> String { let face = match mood { Some(m) if m < 0.3 => " :(", Some(m) if m > 0.7 => " :)", _ => "", }; format!("{name}{}{face}", if child { " (child)" } else { "" }) } #[allow(clippy::type_complexity)] fn rebuild_roster( mut commands: Commands, roster: Single>, colonists: Query<(Entity, &Colonist, Has, Option<&Mood>), With>, mut shown: Local>, ) { let mut wanted: Vec<(Entity, String)> = colonists .iter() .map(|(e, c, child, mood)| (e, roster_label(&c.name, child, mood.map(|m| m.0)))) .collect(); wanted.sort_by_key(|(e, _)| *e); if *shown == wanted { return; } commands.entity(*roster).despawn_related::(); for (entity, label) in &wanted { commands.spawn(( small_button(label.clone()), ColonistButton(*entity), ChildOf(*roster), )); } *shown = wanted; } fn show_work_toggles( selection: Res, adults: Query<(), (With, Without)>, mut row: Single<&mut Node, With>, ) { let visible = matches!(*selection, Selection::Colonist(e) if adults.contains(e)); let display = if visible { Display::Flex } else { Display::None }; if row.display != display { row.display = display; } } #[allow(clippy::type_complexity)] fn style_panel_buttons( selection: Res, priorities: Query<&WorkPriorities>, mut buttons: Query<( &Hovered, Option<&WorkToggle>, Option<&ColonistButton>, &mut BackgroundColor, )>, ) { let selected = match *selection { Selection::Colonist(e) => Some(e), _ => None, }; for (hovered, toggle, colonist, mut color) in &mut buttons { let new = match (toggle, colonist) { (Some(toggle), _) => { let allowed = selected .and_then(|e| priorities.get(e).ok()) .is_none_or(|p| p.allows(toggle.0)); if allowed { BUTTON_ACTIVE_COLOR } else { DISABLED_COLOR } } (None, Some(c)) if Some(c.0) == selected => BUTTON_ACTIVE_COLOR, (None, Some(_)) if hovered.get() => BUTTON_HOVER_COLOR, (None, Some(_)) => BUTTON_COLOR, (None, None) => continue, }; color.set_if_neq(BackgroundColor(new)); } } #[allow(clippy::too_many_arguments)] fn on_panel_activated( activate: On, toggles: Query<&WorkToggle>, colonist_buttons: Query<&ColonistButton>, storage: Query<(Option<&StorageToggle>, Has)>, goal_buttons: Query<&GoalButton>, targets_toggle: Query<(), With>, mut targets: Single<&mut Node, With>, mut selection: ResMut, mut zones: ResMut, mut goals: ResMut, mut priorities: Query<&mut WorkPriorities>, ) { if targets_toggle.contains(activate.entity) { targets.display = match targets.display { Display::None => Display::Flex, _ => Display::None, }; } else if let Ok(button) = goal_buttons.get(activate.entity) { match button.delta { 0 => goals.reset(button.stock), delta => goals.adjust(button.stock, delta), } } else if let Ok((toggle, priority)) = storage.get(activate.entity) && (toggle.is_some() || priority) && let Some(tile) = selected_stockpile(&selection, &zones) { zones.update_region_rule(tile, |rule| match toggle { Some(toggle) => rule.toggle(toggle.0), None => rule.priority = rule.priority.next(), }); } else if let Ok(button) = colonist_buttons.get(activate.entity) { *selection = Selection::Colonist(button.0); } else if let Ok(toggle) = toggles.get(activate.entity) && let Selection::Colonist(entity) = *selection && let Ok(mut work) = priorities.get_mut(entity) { work.toggle(toggle.0); } } #[cfg(test)] mod tests { use super::*; #[test] fn roster_labels_show_children_and_strong_moods() { assert_eq!(roster_label("Ada", false, Some(0.5)), "Ada"); assert_eq!(roster_label("Ivo", true, Some(0.8)), "Ivo (child) :)"); assert_eq!(roster_label("Bram", false, Some(0.1)), "Bram :("); } #[test] fn work_types_can_be_switched_off_and_on() { let mut work = WorkPriorities::default(); assert!(work.allows(WorkType::Haul)); work.toggle(WorkType::Haul); assert!(!work.allows(WorkType::Haul)); assert!(work.allows(WorkType::Food)); work.toggle(WorkType::Haul); assert!(work.allows(WorkType::Haul)); } }