// Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] use games::*; // *TODO: Detect when piece is going to go out of bounds and restirct parent from moving there // TODO: When shape touches the rest of the pieces, re-parent to line entity // TODO: When line is "full" (has 10 children) clear line and add to score fn main() { App::new() .add_plugins(BaseGamePlugin { name: "falling-block-adventure".into(), target_resolution: (640.0, 480.0).into(), game_type: GameType::Two, ..default() }) .init_state::() .add_systems(Startup, (init_world, init_debug_ui)) .add_systems( Update, ( kb_input.run_if(on_event::), falling .run_if(in_state(Falling::On)) .run_if(clock_cycle(1.0)), update_shape_blocks .run_if(any_component_added::.or(any_component_changed::)), sync_singleton_to_ui::.run_if(any_component_changed::), sync_singleton_to_ui::.run_if(any_component_changed::), update_position, add_piece.run_if(not(any_with_component::)), clear_line.run_if(any_component_changed::), ), ) .add_systems(Update, draw_grid) .add_observer(deactive_shape) .run(); } const SCALE: f32 = 30.0; // Declare the size of the play area const X_MAX: u32 = 10; const Y_MAX: u32 = 20; // The blocks making up this shape #[derive(Component)] #[relationship_target(relationship = ShapeBlock)] struct ShapeBlocks(Vec); /// A part of a piece, i.e., a single square of a piece #[derive(Component, Debug)] #[relationship(relationship_target = ShapeBlocks)] struct ShapeBlock { #[relationship] shape: Entity, } // The blocks making up this shape #[derive(Component, Default)] #[relationship_target(relationship = LineBlock)] struct LineBlocks(Vec); /// A part of a piece, i.e., a single square of a piece #[derive(Component, Debug)] #[require(Transform, Visibility)] #[relationship(relationship_target = LineBlocks)] struct LineBlock { #[relationship] line: Entity, } // A line holds up to 10 blocks before being cleared #[derive(Component, Debug)] struct Line(u8); // Just marks a block either of a shape or line #[derive(Component, Debug)] struct Block; #[derive(Component, Debug, Clone, Copy, PartialEq)] #[require(Transform, Visibility)] struct GridPosition { x: u32, y: u32, } impl GridPosition { fn with_offset(self, other_x: isize, other_y: isize) -> Result { let x = self.x as isize + other_x; let y = self.y as isize + other_y; if x >= X_MAX as isize { Err(GameError::OutOfBoundsLeft) } else if x < 0 { Err(GameError::OutOfBoundsRight) } else if y < 0 { Err(GameError::OutOfBoundsDown) } else { Ok(GridPosition { x: x as u32, y: y as u32, }) } } } impl Display for GridPosition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "({},{})", self.x, self.y) } } #[derive(Error, Debug, PartialEq)] enum GameError { #[error("Coordinates are out of bounds: Left")] OutOfBoundsLeft, #[error("Coordinates are out of bounds: Right")] OutOfBoundsRight, #[error("Coordinates are out of bounds: Down")] OutOfBoundsDown, #[error("Coordiante collision")] Collision, } impl Default for GridPosition { fn default() -> Self { GridPosition { x: 5, y: Y_MAX } } } impl From<&GridPosition> for Vec3 { fn from(GridPosition { x, y }: &GridPosition) -> Vec3 { // Grid Positions start in the bottom left of the area // So (0, 0) is the bottom left, (0, 9) is the bottom right, etc let x_0 = -SCALE * 5.0 + (0.5 * SCALE); let x = x_0 + ((*x as f32) * SCALE); let y_0 = -SCALE * 10.0 + (0.5 * SCALE); let y = y_0 + ((*y as f32) * SCALE); Vec3::new(x, y, 0.0) } } impl From<(u32, u32)> for GridPosition { fn from((x, y): (u32, u32)) -> GridPosition { GridPosition { x, y } } } impl std::ops::Add for GridPosition { type Output = Self; fn add(self, GridPosition { x: x2, y: y2 }: Self) -> Self { GridPosition { x: self.x + x2, y: self.y + y2, } } } impl std::ops::AddAssign<&GridPosition> for GridPosition { fn add_assign(&mut self, rhs: &GridPosition) { *self = *self + *rhs; } } #[derive(Component, Default, Event, Clone, Debug)] enum Orientation { #[default] Up, Left, Down, Right, } impl Orientation { fn next(&self) -> Self { match self { Self::Up => Self::Left, Self::Left => Self::Down, Self::Down => Self::Right, Self::Right => Self::Up, } } fn prev(&self) -> Self { match self { Self::Up => Self::Right, Self::Right => Self::Down, Self::Down => Self::Left, Self::Left => Self::Up, } } } impl Display for Orientation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Orientation::Up => write!(f, "up"), Orientation::Down => write!(f, "down"), Orientation::Left => write!(f, "<-"), Orientation::Right => write!(f, "->"), } } } #[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)] enum Falling { #[default] On, Off, } #[derive(Resource, Debug)] struct Visuals { material: Handle, mesh: Handle, } fn init_world( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands.insert_resource(Visuals { material: materials.add(ColorMaterial { color: WHITE.into(), ..default() }), mesh: meshes.add(Rectangle::new(SCALE, SCALE)), }); (0..20).for_each(|i| { commands.spawn((Line(i), LineBlocks::default())); }); } fn init_debug_ui(mut commands: Commands) { commands .spawn(( Node { top: Val::Px(0.0), left: Val::Px(0.0), ..default() }, DebuggingState::On, )) .with_children(|parent| { parent.spawn(( Node::default(), children![ (Text::new("SHAPE"), SyncSingleton::::default()), ( Text::new("ORIENTATION"), SyncSingleton::::default() ), ], )); }); } #[derive(Component, Debug, Clone, Copy)] enum Shape { M4(Mat4), M3(Mat3), } impl Default for Shape { fn default() -> Self { Self::new_t() } } impl Display for Shape { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ascii()) } } impl Shape { fn from_mat4(input: Mat4) -> Self { Self::M4(input) } fn from_mat3(input: Mat3) -> Self { Self::M3(input) } fn new_o() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 0., 0.], [0., 1., 1., 0.], [0., 1., 1., 0.], [0., 0., 0., 0.], ])) } fn new_t() -> Self { Self::from_mat3(Mat3::from_cols_array_2d(&[ [0., 1., 0.], [1., 1., 1.], [0., 0., 0.], ])) } fn new_l() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 1., 0.], ])) } fn new_j() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 0., 0.], [0., 0., 1., 0.], [0., 0., 1., 0.], [0., 1., 1., 0.], ])) } fn new_s() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 0., 0.], [0., 1., 1., 0.], [1., 1., 0., 0.], [0., 0., 0., 0.], ])) } fn new_z() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 0., 0.], [1., 1., 0., 0.], [0., 1., 1., 0.], [0., 0., 0., 0.], ])) } fn new_i() -> Self { Self::from_mat4(Mat4::from_cols_array_2d(&[ [0., 0., 1., 0.], [0., 0., 1., 0.], [0., 0., 1., 0.], [0., 0., 1., 0.], ])) } // Rotates 90 degrees to the right // https://stackoverflow.com/a/8664879 fn rotated(&self) -> Self { match self { Self::M4(inner) => { let mut new_self = inner.transpose(); for i in 0..4 { let col = new_self.col_mut(i); *col = Vec4::new(col[3], col[2], col[1], col[0]); } Self::M4(new_self) } Self::M3(inner) => { let mut new_self = inner.transpose(); for i in 0..3 { let col = new_self.col_mut(i); *col = Vec3::new(col[2], col[1], col[0]); } Self::M3(new_self) } } } fn rotate(&mut self) { *self = self.rotated(); } fn coordinates( &self, center: &GridPosition, ) -> impl Iterator> { let mut v: Vec> = Vec::new(); match self { Self::M4(inner) => { for (i, y) in (-1..3).rev().enumerate() { let c = inner.col(i); for (j, x) in (-1..3).enumerate() { if c[j] == 1.0 { v.push(center.with_offset(x, y)); } } } } Self::M3(inner) => { for (i, y) in (-1..2).rev().enumerate() { let c = inner.col(i); for (j, x) in (-1..2).enumerate() { if c[j] == 1.0 { v.push(center.with_offset(x, y)); } } } } }; v.into_iter() } fn as_ascii(&self) -> String { let mut output = String::default(); match self { Self::M4(this) => { for i in 0..4 { let col = this.col(i).to_array(); output += format!("{}{}{}{}\n", col[0], col[1], col[2], col[3]).as_str(); } } Self::M3(this) => { for i in 0..3 { let col = this.col(i).to_array(); output += format!("{}{}{}\n", col[0], col[1], col[2]).as_str(); } } }; output } } fn update_position( mut changed: Query< (Entity, &GridPosition, &mut Transform), Or<(Added, Changed)>, >, ) { changed.iter_mut().for_each(|(e, gp, mut t)| { let v3: Vec3 = gp.into(); debug!( "Updating {e} with grid position {:?} to coordinates {:?}", gp, v3 ); t.translation = gp.into(); }); } // TODO: Inline this to when movement occurs fn update_shape_blocks( query: Query<(Entity, &Shape, &Orientation, &GridPosition), Or<(Added, Changed)>>, mut blocks: Query<&mut GridPosition, (With, Without)>, mut commands: Commands, visuals: Res, ) { query.iter().for_each(|(e, s, o, center)| { info!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii()); if blocks.is_empty() { let mesh = Mesh2d(visuals.mesh.clone()); let mat = MeshMaterial2d(visuals.material.clone()); commands .entity(e) .with_related_entities::(|parent| { s.coordinates(center).for_each(|gp| { parent .spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block)) .observe(movement); }); }); } else { let mut p = s.coordinates(center); blocks.iter_mut().for_each(|mut gp| { *gp = p.next().unwrap().unwrap(); }); } }); } fn kb_input( mut events: EventReader, mut query: Query<(Entity, &Orientation, &mut Shape)>, curr: Res>, mut next: ResMut>, mut commands: Commands, ) { events.read().for_each( |KeyboardInput { key_code, state, .. }| { if let ButtonState::Pressed = state { query.iter_mut().for_each(|(e, o, mut s)| { match key_code { // Up arrow should rotate if in falling mode // Only move up if in falling::off mode KeyCode::ArrowUp => { commands.entity(e).trigger(Movement::Rotate); } KeyCode::ArrowDown => { commands.entity(e).trigger(Movement::Down); } KeyCode::ArrowLeft => { commands.entity(e).trigger(Movement::Left); } KeyCode::ArrowRight => { commands.entity(e).trigger(Movement::Right); } KeyCode::Space => next.set(match curr.get() { Falling::On => Falling::Off, Falling::Off => Falling::On, }), KeyCode::Digit1 => *s = Shape::new_t(), KeyCode::Digit2 => *s = Shape::new_o(), KeyCode::Digit3 => *s = Shape::new_l(), KeyCode::Digit4 => *s = Shape::new_j(), KeyCode::Digit5 => *s = Shape::new_s(), KeyCode::Digit6 => *s = Shape::new_z(), KeyCode::Digit7 => *s = Shape::new_i(), _ => (), } }); } }, ); } fn draw_grid(mut gizmos: Gizmos) { gizmos .grid_2d( Isometry2d::IDENTITY, UVec2::new(X_MAX, Y_MAX), Vec2::new(SCALE, SCALE), GREEN, ) .outer_edges(); } fn falling(mut shape: Query>, mut commands: Commands) { shape.iter_mut().for_each(|e| { info!("Making {:?} fall", e); commands.entity(e).trigger(Movement::Down); }); } // Run condition that returns `true` every `n` seconds // TODO: Update a resource with the current tick fn clock_cycle(n: f32) -> impl FnMut(Res