1use bevy::{
2 input::{keyboard::KeyboardInput, mouse::MouseButtonInput, ButtonState},
3 math::Vec3Swizzles,
4 prelude::*,
5 utils::HashMap,
6};
7use bevy::window::PrimaryWindow;
8use bevy_match3::prelude::*;
9
10const GEM_SIDE_LENGTH: f32 = 50.0;
11
12fn main() {
13 App::new()
14 .add_plugins(
15 DefaultPlugins
16 .set(WindowPlugin {
17 primary_window: Some(Window {
18 resizable: false,
19 title: "bevy_match3 basic example".to_string(),
20 ..default()
21 }),
22 ..default()
23 }),
24 )
25 .insert_resource(Selection::default())
26 .add_plugins(Match3Plugin)
27 .add_systems(Startup, setup_graphics)
28 .add_systems(Update, (
29 move_to,
30 consume_events,
31 input,
32 visualize_selection,
33 control,
34 animate_once,
35 shuffle))
36 .run();
37}
38
39#[derive(Component, Clone)]
40struct VisibleBoard(HashMap<UVec2, Entity>);
41
42#[derive(Component)]
43struct MainCamera;
44
45fn setup_graphics(mut commands: Commands, board: Res<Board>, asset_server: Res<AssetServer>) {
46 let board_side_length = GEM_SIDE_LENGTH * 10.0;
47 let centered_offset_x = board_side_length / 2.0 - GEM_SIDE_LENGTH / 2.0;
48 let centered_offset_y = board_side_length / 2.0 - GEM_SIDE_LENGTH / 2.0;
49
50 let mut camera = Camera2dBundle::default();
51 camera.transform = Transform::from_xyz(
52 centered_offset_x,
53 0.0 - centered_offset_y,
54 camera.transform.translation.z,
55 );
56 commands.spawn(camera).insert(MainCamera);
57
58 let mut gems = HashMap::default();
59
60 let vis_board = commands.spawn(SpatialBundle::default()).id();
61
62 board.iter().for_each(|(position, typ)| {
63 let transform = Transform::from_xyz(
64 position.x as f32 * GEM_SIDE_LENGTH,
65 position.y as f32 * -GEM_SIDE_LENGTH,
66 0.0,
67 );
68
69 let child = commands
70 .spawn(SpriteBundle {
71 sprite: Sprite {
72 custom_size: Some(Vec2::new(GEM_SIDE_LENGTH, GEM_SIDE_LENGTH)),
73 ..Sprite::default()
74 },
75 transform,
76 texture: asset_server.load(&map_type_to_path(*typ)),
77 ..SpriteBundle::default()
78 })
79 .insert(Name::new(format!("{};{}", position.x, position.y)))
80 .id();
81 gems.insert(*position, child);
82 commands.entity(vis_board).add_child(child);
83 });
84
85
86 let board = VisibleBoard(gems);
87
88 commands.entity(vis_board).insert(board);
89}
90
91fn map_type_to_path(typ: u32) -> String {
92 format!("{typ}.png")
93}
94
95#[derive(Component)]
96struct MoveTo(Vec2);
97
98fn move_to(
99 mut commands: Commands,
100 time: Res<Time>,
101 mut moves: Query<(Entity, &mut Transform, &MoveTo)>,
102) {
103 for (entity, mut transform, MoveTo(move_to)) in moves.iter_mut() {
104 if transform.translation == Vec3::new(move_to.x, move_to.y, transform.translation.z) {
105 commands.entity(entity).remove::<MoveTo>();
106 } else {
107 let mut movement = *move_to - transform.translation.xy();
108 movement = (movement.normalize() * time.delta_seconds() * GEM_SIDE_LENGTH * 5.0).clamp_length_max(movement.length());
110 let movement = movement.extend(transform.translation.z);
111 transform.translation += movement;
112 }
113 }
114}
115
116fn consume_events(
117 mut commands: Commands,
118 mut events: ResMut<BoardEvents>,
119 mut texture_atlases: ResMut<Assets<TextureAtlas>>,
120 mut board_commands: ResMut<BoardCommands>,
121 ass: Res<AssetServer>,
122 mut board: Query<(Entity, &mut VisibleBoard)>,
123 animations: Query<(), With<MoveTo>>,
124) {
125 if animations.iter().count() == 0 {
126 if let Ok(event) = events.pop() {
127 let (board_entity, mut board) = board.single_mut();
128 match event {
129 BoardEvent::Swapped(pos1, pos2) => {
130 let gem1 = board.0.get(&pos1).copied().unwrap();
131 let gem2 = board.0.get(&pos2).copied().unwrap();
132 commands
133 .entity(gem1)
134 .insert(MoveTo(board_pos_to_world_pos(&pos2)));
135
136 commands
137 .entity(gem2)
138 .insert(MoveTo(board_pos_to_world_pos(&pos1)));
139
140 board.0.insert(pos2, gem1);
141 board.0.insert(pos1, gem2);
142 }
143 BoardEvent::Popped(pos) => {
144 let gem = board.0.get(&pos).copied().unwrap();
145 board.0.remove(&pos);
146 commands.entity(gem).despawn_recursive();
147 spawn_explosion(
148 &ass,
149 &mut texture_atlases,
150 &mut commands,
151 &board_pos_to_world_pos(&pos),
152 );
153 }
154 BoardEvent::Matched(matches) => {
155 board_commands
156 .push(BoardCommand::Pop(
157 matches.without_duplicates().iter().copied().collect(),
158 ))
159 .unwrap();
160 }
161 BoardEvent::Dropped(drops) => {
162 let mut new_board = board.clone();
164 for Drop { from, to } in drops {
165 let gem = board.0.get(&from).copied().unwrap();
166 new_board.0.insert(to, gem);
167 new_board.0.remove(&from);
168 commands
169 .entity(gem)
170 .insert(MoveTo(board_pos_to_world_pos(&to)));
171 }
172 *board = new_board;
174 }
175 BoardEvent::Spawned(spawns) => {
176 let mut new_board = board.clone();
177
178 for (pos, typ) in spawns {
179 let world_pos = board_pos_to_world_pos(&pos);
180 let gem = commands
181 .spawn(SpriteBundle {
182 texture: ass.load(&map_type_to_path(typ)),
183 transform: Transform::from_xyz(world_pos.x, 200.0, 0.0),
184 sprite: Sprite {
185 custom_size: Some([50.0, 50.0].into()),
186 ..Sprite::default()
187 },
188 ..SpriteBundle::default()
189 })
190 .insert(MoveTo(world_pos))
191 .id();
192 new_board.0.insert(pos, gem);
193 commands.entity(board_entity).add_child(gem);
194 }
195 *board = new_board;
196 }
197 BoardEvent::Shuffled(moves) => {
198 let mut temp_board = board.clone();
199 for (from, to) in moves {
200 let gem = board.0.get(&from).copied().unwrap();
201
202 commands
203 .entity(gem)
204 .insert(MoveTo(board_pos_to_world_pos(&to)));
205
206 temp_board.0.insert(to, gem);
207 }
208 *board = temp_board;
209 }
210 _ => {
211 println!("Received unimplemented event");
212 }
213 }
214 }
215 }
216}
217
218fn board_pos_to_world_pos(pos: &UVec2) -> Vec2 {
219 Vec2::new(
220 pos.x as f32 * GEM_SIDE_LENGTH,
221 -(pos.y as f32) * GEM_SIDE_LENGTH,
222 )
223}
224
225#[derive(Default, Clone, Copy, Resource)]
226struct Selection(Option<Entity>);
227
228fn input(
229 window_query: Query<&Window, With<PrimaryWindow>>,
230 mut selection: ResMut<Selection>,
231 mut button_events: EventReader<MouseButtonInput>,
232 camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
233 board: Query<&VisibleBoard>,
234) {
235 for event in button_events.read() {
236 if let MouseButtonInput {
237 button: MouseButton::Left,
238 state: ButtonState::Pressed,
239 ..
240 } = event
241 {
242 let window = window_query.single();
243 let (camera, camera_transform) = camera.single();
244 if let Some(world_position) = window.cursor_position()
245 .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
246 .map(|ray| ray.origin.truncate())
247 {
248 let coordinates: IVec2 = (
250 ((world_position.x + GEM_SIDE_LENGTH / 2.0) / GEM_SIDE_LENGTH) as i32,
251 ((GEM_SIDE_LENGTH / 2.0 - world_position.y) / GEM_SIDE_LENGTH) as i32,
252 )
253 .into();
254
255 if coordinates.x >= 0 && coordinates.y >= 0 {
256 selection.0 = board
257 .single()
258 .0
259 .get::<UVec2>(&UVec2::new(coordinates.x as u32, coordinates.y as u32))
260 .copied();
261 }
262 }
263 }
264 }
265}
266
267#[derive(Component)]
268struct SelectionRectangle;
269
270fn visualize_selection(
271 mut commands: Commands,
272 selection: Res<Selection>,
273 ass: Res<AssetServer>,
274 g_transforms: Query<&GlobalTransform>,
275 mut rectangle: Query<(Entity, &mut Transform), With<SelectionRectangle>>,
276) {
277 if selection.is_changed() {
278 if let Some(selected_gem) = selection.0 {
279 let transform = g_transforms.get(selected_gem).unwrap();
280 if let Ok((_, mut old_transform)) = rectangle.get_single_mut() {
281 *old_transform = (*transform).into();
282 } else {
283 commands
284 .spawn(SpriteBundle {
285 texture: ass.load("rectangle.png"),
286 sprite: Sprite {
287 custom_size: Some([50.0, 50.0].into()),
288 ..Sprite::default()
289 },
290 transform: (*transform).into(),
291 ..SpriteBundle::default()
292 })
293 .insert(SelectionRectangle);
294 }
295 } else if let Ok((entity, _)) = rectangle.get_single_mut() {
296 commands.entity(entity).despawn();
297 }
298 }
299}
300
301fn control(
302 mut board_commands: ResMut<BoardCommands>,
303 mut selection: ResMut<Selection>,
304 mut last_selection: Local<Selection>,
305 transforms: Query<&Transform>,
306) {
307 if selection.is_changed() {
308 if let Some(selected_gem) = selection.0 {
309 if let Some(last_selected_gem) = last_selection.0 {
310 let selected_pos = transforms.get(selected_gem).unwrap().translation.xy() / 50.0;
311 let last_selected_pos =
312 transforms.get(last_selected_gem as Entity).unwrap().translation.xy() / 50.0;
313
314 board_commands
315 .push(BoardCommand::Swap(
316 [selected_pos.x as u32, -selected_pos.y as u32].into(),
317 [last_selected_pos.x as u32, -last_selected_pos.y as u32].into(),
318 ))
319 .map_err(|err| println!("{err}"))
320 .unwrap();
321 selection.0 = None;
322 last_selection.0 = None;
323 } else {
324 *last_selection = *selection;
325 }
326 } else {
327 last_selection.0 = None
328 }
329 }
330}
331
332#[derive(Component)]
333struct AnimationTimer(Timer);
334
335fn animate_once(
336 mut commands: Commands,
337 time: Res<Time>,
338 texture_atlases: Res<Assets<TextureAtlas>>,
339 mut timers: Query<(
340 Entity,
341 &mut AnimationTimer,
342 &mut TextureAtlasSprite,
343 &Handle<TextureAtlas>,
344 )>,
345) {
346 for (entity, mut timer, mut sprite, texture_atlas_handle) in timers.iter_mut() {
347 timer.0.tick(time.delta());
348 if timer.0.just_finished() {
349 let texture_atlas = texture_atlases.get(texture_atlas_handle).unwrap();
350 if sprite.index == 3 {
351 commands.entity(entity).despawn_recursive();
352 } else {
353 sprite.index = (sprite.index + 1) % texture_atlas.textures.len();
354 }
355 }
356 }
357}
358
359fn spawn_explosion(
360 ass: &AssetServer,
361 texture_atlases: &mut Assets<TextureAtlas>,
362 commands: &mut Commands,
363 pos: &Vec2,
364) {
365 let texture_handle = ass.load("explosion.png");
366 let texture_atlas =
367 TextureAtlas::from_grid(texture_handle, Vec2::new(49.0, 50.0), 4, 1, None, None);
368 let texture_atlas_handle = texture_atlases.add(texture_atlas);
369 commands
370 .spawn(SpriteSheetBundle {
371 texture_atlas: texture_atlas_handle,
372 transform: Transform::from_translation(pos.extend(0.0)),
373 ..SpriteSheetBundle::default()
374 })
375 .insert(AnimationTimer(Timer::from_seconds(
376 0.1,
377 TimerMode::Repeating,
378 )));
379}
380
381fn shuffle(
382 mut board_commands: ResMut<BoardCommands>,
383 mut key_event: EventReader<KeyboardInput>,
384 animations: Query<(), With<MoveTo>>,
385) {
386 if animations.iter().count() == 0 {
387 for event in key_event.read() {
388 if let KeyboardInput {
389 key_code: Some(KeyCode::S),
390 state: ButtonState::Pressed,
391 ..
392 } = event
393 {
394 board_commands.push(BoardCommand::Shuffle).unwrap();
395 }
396 }
397 }
398}