src/selection.rs 4.9 K · 162 lines · raw · history

1 use bevy::{prelude::*, transform::TransformSystems, window::PrimaryWindow};
2
3 use crate::{
4 camera::CameraController,
5 world::{TILE_SIZE, WorldMap},
6 };
7
8 pub struct SelectionPlugin;
9
10 impl Plugin for SelectionPlugin {
11 fn build(&self, app: &mut App) {
12 app.init_resource::<HoveredTile>()
13 .init_resource::<SelectedTile>()
14 .add_systems(Startup, spawn_tile_info)
15 .add_systems(
16 PostUpdate,
17 (update_hovered, update_selected, draw_highlights, update_tile_info)
18 .chain()
19 .after(TransformSystems::Propagate),
20 );
21 }
22 }
23
24 #[derive(Resource, Default, Debug, PartialEq)]
25 pub struct HoveredTile(pub Option<UVec2>);
26
27 #[derive(Resource, Default, Debug, PartialEq)]
28 pub struct SelectedTile(pub Option<UVec2>);
29
30 #[derive(Component)]
31 struct TileInfoText;
32
33 const HOVER_COLOR: Color = Color::srgba(1.0, 1.0, 1.0, 0.6);
34 const SELECT_COLOR: Color = Color::srgb(1.0, 0.85, 0.2);
35
36 fn update_hovered(
37 map: Res<WorldMap>,
38 window: Single<&Window, With<PrimaryWindow>>,
39 camera: Single<(&Camera, &GlobalTransform), With<CameraController>>,
40 mut hovered: ResMut<HoveredTile>,
41 ) {
42 let (camera, global) = *camera;
43 let tile = window
44 .cursor_position()
45 .and_then(|cursor| camera.viewport_to_world_2d(global, cursor).ok())
46 .and_then(|world| map.tile_at(world));
47 hovered.set_if_neq(HoveredTile(tile));
48 }
49
50 fn update_selected(
51 buttons: Res<ButtonInput<MouseButton>>,
52 keys: Res<ButtonInput<KeyCode>>,
53 hovered: Res<HoveredTile>,
54 mut selected: ResMut<SelectedTile>,
55 ) {
56 if keys.just_pressed(KeyCode::Escape) {
57 selected.set_if_neq(SelectedTile(None));
58 } else if buttons.just_pressed(MouseButton::Left) {
59 selected.set_if_neq(SelectedTile(hovered.0));
60 }
61 }
62
63 fn draw_highlights(mut gizmos: Gizmos, hovered: Res<HoveredTile>, selected: Res<SelectedTile>) {
64 for (tile, color) in [(hovered.0, HOVER_COLOR), (selected.0, SELECT_COLOR)] {
65 if let Some(tile) = tile {
66 gizmos.rect_2d(WorldMap::tile_center(tile), Vec2::splat(TILE_SIZE), color);
67 }
68 }
69 }
70
71 fn spawn_tile_info(mut commands: Commands) {
72 commands.spawn((
73 TileInfoText,
74 Text::default(),
75 TextFont::from_font_size(14.0),
76 Node {
77 position_type: PositionType::Absolute,
78 top: px(8),
79 left: px(8),
80 padding: UiRect::all(px(6)),
81 ..default()
82 },
83 BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.5)),
84 ));
85 }
86
87 fn update_tile_info(
88 map: Res<WorldMap>,
89 hovered: Res<HoveredTile>,
90 selected: Res<SelectedTile>,
91 mut text: Single<&mut Text, With<TileInfoText>>,
92 ) {
93 if !hovered.is_changed() && !selected.is_changed() {
94 return;
95 }
96 text.0 = format!(
97 "Hover: {}\nSelected: {}",
98 describe_tile(&map, hovered.0),
99 describe_tile(&map, selected.0)
100 );
101 }
102
103 pub fn describe_tile(map: &WorldMap, tile: Option<UVec2>) -> String {
104 match tile.and_then(|t| map.get(t.as_ivec2()).map(|terrain| (t, terrain))) {
105 Some((t, terrain)) => format!("({}, {}) {terrain:?}", t.x, t.y),
106 None => "-".into(),
107 }
108 }
109
110 #[cfg(test)]
111 mod tests {
112 use super::*;
113 use crate::world::Terrain;
114
115 fn selection_app(hovered: Option<UVec2>, selected: Option<UVec2>) -> App {
116 let mut app = App::new();
117 app.init_resource::<ButtonInput<MouseButton>>()
118 .init_resource::<ButtonInput<KeyCode>>()
119 .insert_resource(HoveredTile(hovered))
120 .insert_resource(SelectedTile(selected))
121 .add_systems(Update, update_selected);
122 app
123 }
124
125 fn click(app: &mut App) {
126 app.world_mut()
127 .resource_mut::<ButtonInput<MouseButton>>()
128 .press(MouseButton::Left);
129 app.update();
130 }
131
132 #[test]
133 fn clicking_selects_the_hovered_tile() {
134 let mut app = selection_app(Some(UVec2::new(3, 4)), None);
135 click(&mut app);
136 assert_eq!(app.world().resource::<SelectedTile>().0, Some(UVec2::new(3, 4)));
137 }
138
139 #[test]
140 fn clicking_outside_the_world_clears_the_selection() {
141 let mut app = selection_app(None, Some(UVec2::new(3, 4)));
142 click(&mut app);
143 assert_eq!(app.world().resource::<SelectedTile>().0, None);
144 }
145
146 #[test]
147 fn escape_clears_the_selection() {
148 let mut app = selection_app(Some(UVec2::ONE), Some(UVec2::new(3, 4)));
149 app.world_mut()
150 .resource_mut::<ButtonInput<KeyCode>>()
151 .press(KeyCode::Escape);
152 app.update();
153 assert_eq!(app.world().resource::<SelectedTile>().0, None);
154 }
155
156 #[test]
157 fn describe_tile_shows_coordinates_and_terrain() {
158 let map = WorldMap::filled(UVec2::new(4, 4), Terrain::Sand);
159 assert_eq!(describe_tile(&map, Some(UVec2::new(1, 2))), "(1, 2) Sand");
160 assert_eq!(describe_tile(&map, None), "-");
161 }
162 }