src/designations.rs 5.3 K · 183 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 => tool.active = false,
100 _ => continue,
101 }
102 tool.drag_start = None;
103 return;
104 }
105 if !tool.active {
106 return;
107 }
108 if buttons.just_pressed(MouseButton::Left) && !over_ui.0 {
109 tool.drag_start = cursor.tile;
110 }
111 if !buttons.just_released(MouseButton::Left) {
112 return;
113 }
114 let (Some(start), Some(end)) = (tool.drag_start.take(), cursor.tile) else {
115 return;
116 };
117 if removing(&keys) {
118 let (lo, hi) = (start.min(end), start.max(end));
119 designations
120 .0
121 .retain(|t| !(t.cmpge(lo).all() && t.cmple(hi).all()));
122 } else {
123 designate(&map, &mut designations, start, end);
124 }
125 }
126
127 fn draw_designate_preview(
128 keys: Res<ButtonInput<KeyCode>>,
129 cursor: Res<Cursor>,
130 tool: Res<DesignateTool>,
131 mut gizmos: Gizmos,
132 ) {
133 let (Some(start), Some(end)) = (tool.drag_start, cursor.tile) else {
134 return;
135 };
136 let (min, max) = (start.min(end), start.max(end));
137 let center = (WorldMap::tile_center(min) + WorldMap::tile_center(max)) / 2.0;
138 let size = (max - min + 1).as_vec2() * TILE_SIZE;
139 let color = if removing(&keys) {
140 PREVIEW_REMOVE_COLOR
141 } else {
142 PREVIEW_ADD_COLOR
143 };
144 gizmos.rect_2d(center, size, color);
145 }
146
147 fn draw_designations(designations: Res<Designations>, mut gizmos: Gizmos) {
148 let half = TILE_SIZE * 0.3;
149 for &tile in &designations.0 {
150 let c = WorldMap::tile_center(tile);
151 gizmos.line_2d(
152 c - Vec2::splat(half),
153 c + Vec2::splat(half),
154 DESIGNATION_COLOR,
155 );
156 gizmos.line_2d(
157 c + Vec2::new(-half, half),
158 c + Vec2::new(half, -half),
159 DESIGNATION_COLOR,
160 );
161 }
162 }
163
164 #[cfg(test)]
165 mod tests {
166 use super::*;
167 use crate::world::{Plant, Terrain};
168
169 #[test]
170 fn only_trees_and_rock_can_be_designated() {
171 let mut map = WorldMap::filled(UVec2::splat(6), Terrain::Grass);
172 map.set_plant(UVec2::new(1, 1), Some(Plant::Tree));
173 map.set(UVec2::new(2, 2), Terrain::Stone);
174 map.set_plant(UVec2::new(3, 3), Some(Plant::BerryBush));
175 map.set_growth(UVec2::new(3, 3), 1.0);
176 let mut designations = Designations::default();
177 designate(&map, &mut designations, UVec2::ZERO, UVec2::splat(5));
178 assert_eq!(
179 designations.0,
180 HashSet::from([UVec2::new(1, 1), UVec2::new(2, 2)])
181 );
182 }
183 }