diff --git a/README.md b/README.md index d3e0fae..0412ed4 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,5 @@ cargo build **Down arrow:** soft drop **Space bar:** hard drop + +**C:** swap piece diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..da1a381 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# TODO + +- Lose condition +- Speedup +- GUI + - Background + - Indicators for next/stored piece + - Cleared lines counter diff --git a/src/debug.rs b/src/debug.rs index 8eeb9bb..a6dbafd 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -13,9 +13,9 @@ impl Plugin for DebugPlugin { app.configure_sets(PostUpdate, DebugSet.run_if(is_debug_enabled)); app.configure_sets(Last, DebugSet.run_if(is_debug_enabled)); - app.insert_resource(DebugMode::on()) + app.insert_resource(DebugMode::off()) .add_plugins(( - // // WorldInspector requires EguiPlugin plugin to be added before it + // WorldInspector requires EguiPlugin plugin to be added before it bevy_egui::EguiPlugin::default(), bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled), )) diff --git a/src/game.rs b/src/game.rs index c412392..9a7570d 100644 --- a/src/game.rs +++ b/src/game.rs @@ -16,13 +16,18 @@ pub fn init(app: &mut App) { )); app.add_event::() + .add_event::() + .add_event::() + .add_event::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -30,6 +35,15 @@ pub fn init(app: &mut App) { .register_type::() .add_systems(Startup, systems::setup_game_scene) .add_systems(PreUpdate, (systems::repeat_inputs, systems::apply_gravity)) - .add_systems(Update, (systems::demo_2d, systems::apply_piece_movement)) + .add_systems( + Update, + ( + systems::demo_2d, + systems::store_piece, + systems::apply_piece_movement, + systems::trigger_on_next_piece_changed, + systems::trigger_on_stored_piece_changed, + ), + ) .add_systems(PostUpdate, systems::grid_positioning); } diff --git a/src/game/prefab.rs b/src/game/prefab.rs index 7a7b53e..a47b607 100644 --- a/src/game/prefab.rs +++ b/src/game/prefab.rs @@ -65,7 +65,7 @@ impl PieceType { Self::J => [(-1, 0), (-1, 1), (0, 0), (1, 0)], Self::S => [(-1, 0), (0, 0), (0, 1), (1, 1)], Self::Z => [(-1, 1), (0, 0), (0, 1), (1, 0)], - Self::Square => [(0, 0), (0, 1), (1, 0), (1, 1)], + Self::Square => [(-1, 0), (-1, 1), (0, 0), (0, 1)], } .map(|pos| pos.into()) } diff --git a/src/game/systems.rs b/src/game/systems.rs index f93944a..5f9a243 100644 --- a/src/game/systems.rs +++ b/src/game/systems.rs @@ -3,9 +3,11 @@ mod game_scene; mod grid; mod input; mod movement; +mod ui; pub use demo::*; pub use game_scene::*; pub use grid::*; pub use input::*; pub use movement::*; +pub use ui::*; diff --git a/src/game/systems/game_scene.rs b/src/game/systems/game_scene.rs index 284c96f..796aba5 100644 --- a/src/game/systems/game_scene.rs +++ b/src/game/systems/game_scene.rs @@ -8,30 +8,131 @@ pub fn setup_game_scene(world: &mut World) { let game_area = GameArea::default(); let mut next_piece = NextPiece::default(); let starting_piece = next_piece.take_and_generate(); - world - .spawn(( - Name::from("Game scene"), - GameGravity::default(), - game_area.clone(), - next_piece, - children![ - (Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,), - // Create first piece - ( - GridTransform { - translation: game_area.block_spawn_point() - }, - ControllablePiece, - create_piece(starting_piece), - ), - ], - )) + + let mut game_entity = world.spawn(( + Name::from("Game scene"), + GameGravity::default(), + GameStats::default(), + game_area.clone(), + next_piece, + children![ + (Transform::from_xyz(36.0, 88.0, 10.0), DemoCamera2d,), + // Create first piece + ( + GridTransform { + translation: game_area.block_spawn_point() + }, + ControllablePiece, + create_piece(starting_piece), + ), + ], + )); + + game_entity .observe(handle_placed_piece) .observe(handle_line_clear); + + // Next piece indicator + game_entity.with_child(( + Name::from("Next piece indicator"), + Grid::default(), + GridTransform::from_xy(13, 18), + Observer::new(update_next_piece_ui).with_entity(game_entity.id()), + )); + + // Stored piece indicator + game_entity.with_child(( + Name::from("Stored piece indicator"), + Grid::default(), + GridTransform::from_xy(-4, 18), + Observer::new(update_stored_piece_ui).with_entity(game_entity.id()), + )); + world.flush(); } -pub fn handle_placed_piece(trigger: Trigger, world: &mut World) { +pub fn store_piece( + mut commands: Commands, + piece_query: Query<(Entity, &PieceControls, &PieceType, &ChildOf), Without>, + mut store_query: Query<(Option<&mut StoredPiece>, &GameArea)>, + mut next_piece_query: Query<&mut NextPiece>, +) { + for (old_entity, controls, piece, parent) in piece_query.iter() { + if controls.store + && let Ok((maybes_stored_piece, game_area)) = store_query.get_mut(parent.parent()) + { + let mut game = commands.entity(parent.parent()); + match maybes_stored_piece { + Some(mut stored_piece) => { + game.with_child(( + GridTransform { + translation: game_area.block_spawn_point(), + }, + ControllablePiece, + SwappedPiece, + create_piece(stored_piece.piece), + )); + stored_piece.piece = *piece; + commands.entity(old_entity).despawn(); + } + None => { + if let Ok(mut next_piece) = next_piece_query.get_mut(parent.parent()) { + game.insert(StoredPiece { piece: *piece }); + game.with_child(( + GridTransform { + translation: game_area.block_spawn_point(), + }, + ControllablePiece, + SwappedPiece, + create_piece(next_piece.take_and_generate()), + )); + commands.entity(old_entity).despawn(); + } + } + } + } + } +} + +fn update_next_piece_ui( + trigger: Trigger, + mut commands: Commands, + parent_query: Query<&ChildOf>, + next_piece_query: Query<&NextPiece>, +) { + if let Ok(mut indicator) = commands.get_entity(trigger.observer()) { + // Despawn children recursively + indicator.despawn_related::(); + + if let Ok(next_piece) = parent_query + .get(indicator.id()) + .and_then(|p| next_piece_query.get(p.parent())) + { + indicator.with_child((Grid::default(), create_piece(next_piece.piece))); + } + } +} + +fn update_stored_piece_ui( + trigger: Trigger, + mut commands: Commands, + parent_query: Query<&ChildOf>, + stored_piece_query: Query<&StoredPiece>, +) { + if let Ok(mut indicator) = commands.get_entity(trigger.observer()) { + // Despawn children recursively + indicator.despawn_related::(); + + if let Ok(stored_piece) = parent_query + .get(indicator.id()) + .and_then(|p| stored_piece_query.get(p.parent())) + { + indicator.with_child((Grid::default(), create_piece(stored_piece.piece))); + } + } +} + +fn handle_placed_piece(trigger: Trigger, world: &mut World) { let game_area = trigger.target(); let event = *trigger.event(); @@ -65,8 +166,6 @@ pub fn handle_placed_piece(trigger: Trigger, world: &mut World) { .run_system_cached_with(create_next_piece, game_area) .expect("Creating new piece"); world.flush(); - - // TODO: Lose condition } fn rebuild_collisions( @@ -155,7 +254,7 @@ fn check_full_rows( cleared_rows } -fn check_lose_condition(In(game_area_entity): In) {} +fn check_lose_condition(In(_game_area_entity): In) {} fn create_next_piece( In(game_area_entity): In, @@ -174,13 +273,13 @@ fn create_next_piece( fn handle_line_clear( trigger: Trigger, - game_area_query: Query<&Children>, + mut game_area_query: Query<(&Children, Option<&mut GameStats>)>, mut block_query: Query<(Entity, &mut GridTransform), With>, mut commands: Commands, ) { let lines = trigger.event().lines.clone(); - let children = game_area_query - .get(trigger.target()) + let (children, maybe_stats) = game_area_query + .get_mut(trigger.target()) .expect("Getting GameArea component to clear rows"); enum BlockOperation { @@ -221,4 +320,8 @@ fn handle_line_clear( _ => (), }; } + + if let Some(mut stats) = maybe_stats { + stats.lines_cleared += lines.len() as i32; + } } diff --git a/src/game/systems/input.rs b/src/game/systems/input.rs index a46285d..b7858f9 100644 --- a/src/game/systems/input.rs +++ b/src/game/systems/input.rs @@ -49,5 +49,9 @@ pub fn repeat_inputs( if key_input.just_pressed(KeyCode::Space) { controls.instant_drop = true; } + + if key_input.just_pressed(KeyCode::KeyC) { + controls.store = true; + } } } diff --git a/src/game/systems/ui.rs b/src/game/systems/ui.rs new file mode 100644 index 0000000..d3d43bd --- /dev/null +++ b/src/game/systems/ui.rs @@ -0,0 +1,31 @@ +use bevy::prelude::*; + +use crate::game::tetris::*; + +pub fn trigger_on_next_piece_changed( + mut commands: Commands, + next_piece_query: Query<(Entity, &NextPiece), Changed>, +) { + for (entity, changed) in next_piece_query.iter() { + commands.trigger_targets( + OnNextPieceChanged { + piece: changed.piece, + }, + entity, + ); + } +} + +pub fn trigger_on_stored_piece_changed( + mut commands: Commands, + next_piece_query: Query<(Entity, &StoredPiece), Changed>, +) { + for (entity, changed) in next_piece_query.iter() { + commands.trigger_targets( + OnStoredPieceChanged { + piece: changed.piece, + }, + entity, + ); + } +} diff --git a/src/game/tetris.rs b/src/game/tetris.rs index 80d62ca..06e72df 100644 --- a/src/game/tetris.rs +++ b/src/game/tetris.rs @@ -11,32 +11,9 @@ pub mod input; pub type YPos = i32; pub type XPos = i32; -#[derive(Component, Debug, Default, Reflect)] -#[reflect(Component)] -pub struct BlockSet { - blocks: HashSet, -} - -impl BlockSet { - pub fn new(blocks: HashSet) -> Self { - Self { blocks } - } - - pub fn cast_point(&self, pos: &Vector2I) -> bool { - self.blocks.contains(pos) - } - - pub fn cast_shape(&self, shape_pos: Vector2I, local_blocks: &[Vector2I]) -> bool { - local_blocks - .iter() - .map(|local_pos| shape_pos + *local_pos) - .any(|global_pos| self.cast_point(&global_pos)) - } -} - #[derive(Component, Clone, Debug, Reflect)] #[reflect(Component)] -#[require(Grid, NextPiece, BlockSet)] +#[require(Grid, NextPiece, BlockSet, GameStats)] pub struct GameArea { pub bottom_boundary: i32, pub kill_height: i32, @@ -90,6 +67,35 @@ impl GameArea { } } +#[derive(Component, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct GameStats { + pub lines_cleared: i32, +} + +#[derive(Component, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct BlockSet { + blocks: HashSet, +} + +impl BlockSet { + pub fn new(blocks: HashSet) -> Self { + Self { blocks } + } + + pub fn cast_point(&self, pos: &Vector2I) -> bool { + self.blocks.contains(pos) + } + + pub fn cast_shape(&self, shape_pos: Vector2I, local_blocks: &[Vector2I]) -> bool { + local_blocks + .iter() + .map(|local_pos| shape_pos + *local_pos) + .any(|global_pos| self.cast_point(&global_pos)) + } +} + #[derive(Component, Debug, Reflect)] #[reflect(Component)] pub struct GameGravity { @@ -122,6 +128,7 @@ pub struct PieceControls { pub fast_drop: bool, pub movement: Option, pub rotation: Option, + pub store: bool, } #[derive(Component, Debug, Reflect)] @@ -129,12 +136,23 @@ pub struct PieceControls { #[require(PieceControls)] pub struct ControllablePiece; +/// Indicates that the piece was already swapped, so it can't be swapped again. +#[derive(Component, Debug, Reflect)] +#[reflect(Component)] +pub struct SwappedPiece; + #[derive(Component, Clone, Debug, Reflect)] #[reflect(Component)] pub struct NextPiece { pub piece: PieceType, } +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component)] +pub struct StoredPiece { + pub piece: PieceType, +} + impl Default for NextPiece { fn default() -> Self { Self { @@ -168,3 +186,13 @@ pub struct OnPiecePlaced { pub struct OnLinesCleared { pub lines: Vec, } + +#[derive(Event, Clone, Debug)] +pub struct OnNextPieceChanged { + pub piece: PieceType, +} + +#[derive(Event, Clone, Debug)] +pub struct OnStoredPieceChanged { + pub piece: PieceType, +}