Added piece swapping and an indicator for the next piece

master
hheik 2025-08-24 22:08:58 +03:00
parent c4275f90be
commit 56b99d9c9a
10 changed files with 245 additions and 53 deletions

View File

@ -25,3 +25,5 @@ cargo build
**Down arrow:** soft drop **Down arrow:** soft drop
**Space bar:** hard drop **Space bar:** hard drop
**C:** swap piece

8
TODO.md Normal file
View File

@ -0,0 +1,8 @@
# TODO
- Lose condition
- Speedup
- GUI
- Background
- Indicators for next/stored piece
- Cleared lines counter

View File

@ -13,9 +13,9 @@ impl Plugin for DebugPlugin {
app.configure_sets(PostUpdate, DebugSet.run_if(is_debug_enabled)); app.configure_sets(PostUpdate, DebugSet.run_if(is_debug_enabled));
app.configure_sets(Last, 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(( .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_egui::EguiPlugin::default(),
bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled), bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled),
)) ))

View File

@ -16,13 +16,18 @@ pub fn init(app: &mut App) {
)); ));
app.add_event::<tetris::OnPiecePlaced>() app.add_event::<tetris::OnPiecePlaced>()
.add_event::<tetris::OnLinesCleared>()
.add_event::<tetris::OnStoredPieceChanged>()
.add_event::<tetris::OnNextPieceChanged>()
.register_type::<tetris::GameArea>() .register_type::<tetris::GameArea>()
.register_type::<tetris::GameStats>()
.register_type::<tetris::GameGravity>() .register_type::<tetris::GameGravity>()
.register_type::<tetris::BlockSet>() .register_type::<tetris::BlockSet>()
.register_type::<tetris::NextPiece>() .register_type::<tetris::NextPiece>()
.register_type::<tetris::Piece>() .register_type::<tetris::Piece>()
.register_type::<tetris::PieceControls>() .register_type::<tetris::PieceControls>()
.register_type::<tetris::ControllablePiece>() .register_type::<tetris::ControllablePiece>()
.register_type::<tetris::SwappedPiece>()
.register_type::<tetris::Gravity>() .register_type::<tetris::Gravity>()
.register_type::<tetris::Block>() .register_type::<tetris::Block>()
.register_type::<prefab::PieceType>() .register_type::<prefab::PieceType>()
@ -30,6 +35,15 @@ pub fn init(app: &mut App) {
.register_type::<grid::GridTransform>() .register_type::<grid::GridTransform>()
.add_systems(Startup, systems::setup_game_scene) .add_systems(Startup, systems::setup_game_scene)
.add_systems(PreUpdate, (systems::repeat_inputs, systems::apply_gravity)) .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); .add_systems(PostUpdate, systems::grid_positioning);
} }

View File

@ -65,7 +65,7 @@ impl PieceType {
Self::J => [(-1, 0), (-1, 1), (0, 0), (1, 0)], Self::J => [(-1, 0), (-1, 1), (0, 0), (1, 0)],
Self::S => [(-1, 0), (0, 0), (0, 1), (1, 1)], Self::S => [(-1, 0), (0, 0), (0, 1), (1, 1)],
Self::Z => [(-1, 1), (0, 0), (0, 1), (1, 0)], 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()) .map(|pos| pos.into())
} }

View File

@ -3,9 +3,11 @@ mod game_scene;
mod grid; mod grid;
mod input; mod input;
mod movement; mod movement;
mod ui;
pub use demo::*; pub use demo::*;
pub use game_scene::*; pub use game_scene::*;
pub use grid::*; pub use grid::*;
pub use input::*; pub use input::*;
pub use movement::*; pub use movement::*;
pub use ui::*;

View File

@ -8,30 +8,131 @@ pub fn setup_game_scene(world: &mut World) {
let game_area = GameArea::default(); let game_area = GameArea::default();
let mut next_piece = NextPiece::default(); let mut next_piece = NextPiece::default();
let starting_piece = next_piece.take_and_generate(); let starting_piece = next_piece.take_and_generate();
world
.spawn(( let mut game_entity = world.spawn((
Name::from("Game scene"), Name::from("Game scene"),
GameGravity::default(), GameGravity::default(),
game_area.clone(), GameStats::default(),
next_piece, game_area.clone(),
children![ next_piece,
(Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,), children![
// Create first piece (Transform::from_xyz(36.0, 88.0, 10.0), DemoCamera2d,),
( // Create first piece
GridTransform { (
translation: game_area.block_spawn_point() GridTransform {
}, translation: game_area.block_spawn_point()
ControllablePiece, },
create_piece(starting_piece), ControllablePiece,
), create_piece(starting_piece),
], ),
)) ],
));
game_entity
.observe(handle_placed_piece) .observe(handle_placed_piece)
.observe(handle_line_clear); .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(); world.flush();
} }
pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) { pub fn store_piece(
mut commands: Commands,
piece_query: Query<(Entity, &PieceControls, &PieceType, &ChildOf), Without<SwappedPiece>>,
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<OnNextPieceChanged>,
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::<Children>();
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<OnStoredPieceChanged>,
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::<Children>();
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<OnPiecePlaced>, world: &mut World) {
let game_area = trigger.target(); let game_area = trigger.target();
let event = *trigger.event(); let event = *trigger.event();
@ -65,8 +166,6 @@ pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
.run_system_cached_with(create_next_piece, game_area) .run_system_cached_with(create_next_piece, game_area)
.expect("Creating new piece"); .expect("Creating new piece");
world.flush(); world.flush();
// TODO: Lose condition
} }
fn rebuild_collisions( fn rebuild_collisions(
@ -155,7 +254,7 @@ fn check_full_rows(
cleared_rows cleared_rows
} }
fn check_lose_condition(In(game_area_entity): In<Entity>) {} fn check_lose_condition(In(_game_area_entity): In<Entity>) {}
fn create_next_piece( fn create_next_piece(
In(game_area_entity): In<Entity>, In(game_area_entity): In<Entity>,
@ -174,13 +273,13 @@ fn create_next_piece(
fn handle_line_clear( fn handle_line_clear(
trigger: Trigger<OnLinesCleared>, trigger: Trigger<OnLinesCleared>,
game_area_query: Query<&Children>, mut game_area_query: Query<(&Children, Option<&mut GameStats>)>,
mut block_query: Query<(Entity, &mut GridTransform), With<Block>>, mut block_query: Query<(Entity, &mut GridTransform), With<Block>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let lines = trigger.event().lines.clone(); let lines = trigger.event().lines.clone();
let children = game_area_query let (children, maybe_stats) = game_area_query
.get(trigger.target()) .get_mut(trigger.target())
.expect("Getting GameArea component to clear rows"); .expect("Getting GameArea component to clear rows");
enum BlockOperation { enum BlockOperation {
@ -221,4 +320,8 @@ fn handle_line_clear(
_ => (), _ => (),
}; };
} }
if let Some(mut stats) = maybe_stats {
stats.lines_cleared += lines.len() as i32;
}
} }

View File

@ -49,5 +49,9 @@ pub fn repeat_inputs(
if key_input.just_pressed(KeyCode::Space) { if key_input.just_pressed(KeyCode::Space) {
controls.instant_drop = true; controls.instant_drop = true;
} }
if key_input.just_pressed(KeyCode::KeyC) {
controls.store = true;
}
} }
} }

31
src/game/systems/ui.rs Normal file
View File

@ -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<NextPiece>>,
) {
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<StoredPiece>>,
) {
for (entity, changed) in next_piece_query.iter() {
commands.trigger_targets(
OnStoredPieceChanged {
piece: changed.piece,
},
entity,
);
}
}

View File

@ -11,32 +11,9 @@ pub mod input;
pub type YPos = i32; pub type YPos = i32;
pub type XPos = i32; pub type XPos = i32;
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct BlockSet {
blocks: HashSet<Vector2I>,
}
impl BlockSet {
pub fn new(blocks: HashSet<Vector2I>) -> 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)] #[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[require(Grid, NextPiece, BlockSet)] #[require(Grid, NextPiece, BlockSet, GameStats)]
pub struct GameArea { pub struct GameArea {
pub bottom_boundary: i32, pub bottom_boundary: i32,
pub kill_height: 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<Vector2I>,
}
impl BlockSet {
pub fn new(blocks: HashSet<Vector2I>) -> 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)] #[derive(Component, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct GameGravity { pub struct GameGravity {
@ -122,6 +128,7 @@ pub struct PieceControls {
pub fast_drop: bool, pub fast_drop: bool,
pub movement: Option<input::Move>, pub movement: Option<input::Move>,
pub rotation: Option<input::Rotation>, pub rotation: Option<input::Rotation>,
pub store: bool,
} }
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
@ -129,12 +136,23 @@ pub struct PieceControls {
#[require(PieceControls)] #[require(PieceControls)]
pub struct ControllablePiece; 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)] #[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct NextPiece { pub struct NextPiece {
pub piece: PieceType, pub piece: PieceType,
} }
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct StoredPiece {
pub piece: PieceType,
}
impl Default for NextPiece { impl Default for NextPiece {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -168,3 +186,13 @@ pub struct OnPiecePlaced {
pub struct OnLinesCleared { pub struct OnLinesCleared {
pub lines: Vec<YPos>, pub lines: Vec<YPos>,
} }
#[derive(Event, Clone, Debug)]
pub struct OnNextPieceChanged {
pub piece: PieceType,
}
#[derive(Event, Clone, Debug)]
pub struct OnStoredPieceChanged {
pub piece: PieceType,
}