src/designations.rs 5.3 K · 184 lines · raw · history

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