src/designations.rs 5.2 K · 182 lines · raw · history

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 }