1use bevy::{
4 platform::collections::{HashMap, HashSet},
5 prelude::*,
6};
7use rand::{Rng, SeedableRng};
8use rand_chacha::ChaCha8Rng;
9
10fn main() {
11 App::new()
12 .add_plugins(DefaultPlugins)
13 .init_resource::<SpatialIndex>()
14 .add_systems(Startup, setup)
15 .add_systems(Update, (draw_shapes, handle_click))
16 .add_observer(
19 |trigger: Trigger<ExplodeMines>,
20 mines: Query<&Mine>,
21 index: Res<SpatialIndex>,
22 mut commands: Commands| {
23 let event = trigger.event();
25 for e in index.get_nearby(event.pos) {
27 let mine = mines.get(e).unwrap();
29 if mine.pos.distance(event.pos) < mine.size + event.radius {
30 commands.trigger_targets(Explode, e);
33 }
34 }
35 },
36 )
37 .add_observer(on_add_mine)
39 .add_observer(on_remove_mine)
42 .run();
43}
44
45#[derive(Component)]
46struct Mine {
47 pos: Vec2,
48 size: f32,
49}
50
51impl Mine {
52 fn random(rand: &mut ChaCha8Rng) -> Self {
53 Mine {
54 pos: Vec2::new(
55 (rand.r#gen::<f32>() - 0.5) * 1200.0,
56 (rand.r#gen::<f32>() - 0.5) * 600.0,
57 ),
58 size: 4.0 + rand.r#gen::<f32>() * 16.0,
59 }
60 }
61}
62
63#[derive(Event)]
64struct ExplodeMines {
65 pos: Vec2,
66 radius: f32,
67}
68
69#[derive(Event)]
70struct Explode;
71
72fn setup(mut commands: Commands) {
73 commands.spawn(Camera2d);
74 commands.spawn((
75 Text::new(
76 "Click on a \"Mine\" to trigger it.\n\
77 When it explodes it will trigger all overlapping mines.",
78 ),
79 Node {
80 position_type: PositionType::Absolute,
81 top: Val::Px(12.),
82 left: Val::Px(12.),
83 ..default()
84 },
85 ));
86
87 let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
88
89 commands
90 .spawn(Mine::random(&mut rng))
91 .observe(explode_mine);
95
96 let mut observer = Observer::new(explode_mine);
104
105 for _ in 0..1000 {
107 let entity = commands.spawn(Mine::random(&mut rng)).id();
108 observer.watch_entity(entity);
109 }
110
111 commands.spawn(observer);
113}
114
115fn on_add_mine(
116 trigger: Trigger<OnAdd, Mine>,
117 query: Query<&Mine>,
118 mut index: ResMut<SpatialIndex>,
119) {
120 let mine = query.get(trigger.target()).unwrap();
121 let tile = (
122 (mine.pos.x / CELL_SIZE).floor() as i32,
123 (mine.pos.y / CELL_SIZE).floor() as i32,
124 );
125 index.map.entry(tile).or_default().insert(trigger.target());
126}
127
128fn on_remove_mine(
130 trigger: Trigger<OnRemove, Mine>,
131 query: Query<&Mine>,
132 mut index: ResMut<SpatialIndex>,
133) {
134 let mine = query.get(trigger.target()).unwrap();
135 let tile = (
136 (mine.pos.x / CELL_SIZE).floor() as i32,
137 (mine.pos.y / CELL_SIZE).floor() as i32,
138 );
139 index.map.entry(tile).and_modify(|set| {
140 set.remove(&trigger.target());
141 });
142}
143
144fn explode_mine(trigger: Trigger<Explode>, query: Query<&Mine>, mut commands: Commands) {
145 let id = trigger.target();
147 let Ok(mut entity) = commands.get_entity(id) else {
148 return;
149 };
150 info!("Boom! {} exploded.", id.index());
151 entity.despawn();
152 let mine = query.get(id).unwrap();
153 commands.trigger(ExplodeMines {
155 pos: mine.pos,
156 radius: mine.size,
157 });
158}
159
160fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
162 for mine in &mines {
163 gizmos.circle_2d(
164 mine.pos,
165 mine.size,
166 Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
167 );
168 }
169}
170
171fn handle_click(
173 mouse_button_input: Res<ButtonInput<MouseButton>>,
174 camera: Single<(&Camera, &GlobalTransform)>,
175 windows: Query<&Window>,
176 mut commands: Commands,
177) {
178 let Ok(windows) = windows.single() else {
179 return;
180 };
181
182 let (camera, camera_transform) = *camera;
183 if let Some(pos) = windows
184 .cursor_position()
185 .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
186 .map(|ray| ray.origin.truncate())
187 {
188 if mouse_button_input.just_pressed(MouseButton::Left) {
189 commands.trigger(ExplodeMines { pos, radius: 1.0 });
190 }
191 }
192}
193
194#[derive(Resource, Default)]
195struct SpatialIndex {
196 map: HashMap<(i32, i32), HashSet<Entity>>,
197}
198
199const CELL_SIZE: f32 = 64.0;
201
202impl SpatialIndex {
203 fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
205 let tile = (
206 (pos.x / CELL_SIZE).floor() as i32,
207 (pos.y / CELL_SIZE).floor() as i32,
208 );
209 let mut nearby = Vec::new();
210 for x in -1..2 {
211 for y in -1..2 {
212 if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
213 nearby.extend(mines.iter());
214 }
215 }
216 }
217 nearby
218 }
219}