From 6fd9550975e7bb0cca8e846104ebb7d4918eda8e Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 17 Nov 2025 21:50:20 -0800 Subject: [PATCH] Shape colors This breaks shapes out into having separate colors. This requires breaking shapes out into an enum describing the shape and the shape layout, then we can add the mesh and material based on the shape enum variant. --- src/bin/tetris/main.rs | 353 ++++++++++++++++++++++------------------- src/lib.rs | 10 +- 2 files changed, 194 insertions(+), 169 deletions(-) diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index f4de31c..070ed90 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -35,7 +35,6 @@ fn main() { init_cameras, init_ui.after(init_cameras), init_battler, - init_deck, ), ) // Input and basic systems @@ -56,10 +55,10 @@ fn main() { .after(update_next_shapes), falling .run_if(in_state(GameState::Falling)) - .run_if(clock_cycle(1.0)), + .run_if(on_timer(Duration::from_secs(1))), update_position.run_if(any_component_changed::), update_shape_blocks - .run_if(any_component_added::.or(any_component_changed::)) + .run_if(any_component_changed::.or(any_component_changed::)) .after(update_position), deactivate_shape .run_if(any_component_removed::) @@ -74,7 +73,7 @@ fn main() { .run_if(any_component_changed::.or(any_component_added::)), damage_on_place_shape.run_if(any_component_removed::), damage_on_clear_line.run_if(any_component_removed::), - damage_over_time.run_if(clock_cycle(5.0)), + damage_over_time.run_if(on_timer(Duration::from_secs(5))), ), ) // UI systems @@ -88,6 +87,8 @@ fn main() { ), ) .add_observer(deal_damage) + .add_observer(on_add_shape_layout) + .add_observer(on_add_health) .run(); } @@ -227,8 +228,14 @@ enum GameState { #[derive(Resource, Debug)] struct Visuals { - material: Handle, mesh: Handle, + material_o: Handle, + material_t: Handle, + material_l: Handle, + material_j: Handle, + material_s: Handle, + material_z: Handle, + material_i: Handle, } #[derive(Resource, Debug, Default)] @@ -266,7 +273,7 @@ struct ShapeStore(Option); impl Display for ShapeStore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.0 { - Some(inner) => write!(f, "{}", inner.as_ascii()), + Some(s) => write!(f, "{}", ShapeLayout::from_shape(s).as_ascii()), None => write!(f, "---"), } } @@ -281,17 +288,36 @@ fn init_tetris( mut materials: ResMut>, ) { let mesh = meshes.add(Rectangle::new(SCALE, SCALE)); - let block_material = materials.add(ColorMaterial { - color: WHITE.into(), - ..default() - }); - let grid_material = materials.add(ColorMaterial { - color: BLACK.into(), - ..default() - }); commands.insert_resource(Visuals { - material: block_material.clone(), mesh: mesh.clone(), + material_o: materials.add(ColorMaterial { + color: YELLOW.into(), + ..default() + }), + material_t: materials.add(ColorMaterial { + color: PURPLE.into(), + ..default() + }), + material_l: materials.add(ColorMaterial { + color: ORANGE.into(), + ..default() + }), + material_j: materials.add(ColorMaterial { + color: BLUE.into(), + ..default() + }), + material_s: materials.add(ColorMaterial { + color: LIME.into(), + ..default() + }), + material_z: materials.add(ColorMaterial { + color: RED.into(), + ..default() + }), + material_i: materials.add(ColorMaterial { + color: AQUA.into(), + ..default() + }), }); (0..Y_MAX).for_each(|y| { @@ -299,7 +325,10 @@ fn init_tetris( (0..X_MAX).for_each(|x| { commands.spawn(( Mesh2d(mesh.clone()), - MeshMaterial2d(grid_material.clone()), + MeshMaterial2d(materials.add(ColorMaterial { + color: BLACK.into(), + ..default() + })), GridPosition { x, y }, Transform::from_xyz(0.0, 0.0, -1.0), GridBackground, @@ -312,12 +341,6 @@ fn init_tetris( }); } -fn init_deck( - mut commands: Commands, -) { - todo!() -} - #[derive(Component, Debug)] struct Protagonist; @@ -579,32 +602,63 @@ fn init_debug_ui(mut commands: Commands) { }); } +/// Enum describing a shape option #[derive(Component, Debug, Clone)] -struct Shape { - layout: ShapeBlockLayout, +enum Shape { + O, + T, + L, + J, + S, + Z, + I, } impl Default for Shape { fn default() -> Self { - Self::new_t() + Self::T } } impl Display for Shape { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.layout.as_ascii()) + write!(f, "{self:?}") } } -impl From>> for Shape { - fn from(inner: Vec>) -> Shape { - Shape { - layout: ShapeBlockLayout { inner }, - } +impl Shape { + fn reposition( + (x_offset, y_offset): (isize, isize), + center: &GridPosition, + ) -> Result { + center.with_offset(x_offset, y_offset) } } -impl Shape { +#[derive(Component, Debug, Clone, PartialEq, Eq)] +struct ShapeLayout { + inner: Vec>, +} + +impl From>> for ShapeLayout { + fn from(inner: Vec>) -> Self { + Self { inner } + } +} + +impl ShapeLayout { + fn from_shape(s: &Shape) -> Self { + match s { + Shape::O => Self::new_o(), + Shape::T => Self::new_t(), + Shape::L => Self::new_l(), + Shape::J => Self::new_j(), + Shape::S => Self::new_s(), + Shape::Z => Self::new_z(), + Shape::I => Self::new_i(), + } + } + fn new_o() -> Self { vec![vec![1, 1], vec![1, 1]].into() } @@ -637,45 +691,6 @@ impl Shape { vec![vec![1], vec![1], vec![1], vec![1]].into() } - // Rotates 90 degrees to the right - // https://stackoverflow.com/a/8664879 - fn rotated(&self) -> Self { - Self { - layout: self.layout.rotated(), - } - } - - fn reposition( - (x_offset, y_offset): (isize, isize), - center: &GridPosition, - ) -> Result { - center.with_offset(x_offset, y_offset) - } - - fn coordinates( - &self, - center: &GridPosition, - ) -> impl Iterator> { - self.layout - .coordinates() - .map(|(x, y)| Shape::reposition((x, y), center)) - } - - fn as_ascii(&self) -> String { - self.layout.as_ascii() - } - - fn height(&self) -> usize { - self.layout.height() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ShapeBlockLayout { - inner: Vec>, -} - -impl ShapeBlockLayout { fn rotated(&self) -> Self { let mut inner = vec![]; for _ in 0..self.inner[0].len() { @@ -686,7 +701,7 @@ impl ShapeBlockLayout { inner[j].insert(0, *x); } } - ShapeBlockLayout { inner } + ShapeLayout { inner } } fn center(&self) -> (usize, usize) { @@ -709,6 +724,14 @@ impl ShapeBlockLayout { (mid_x, mid_y) } + fn coordinates_at( + &self, + center: &GridPosition, + ) -> impl Iterator> { + self.coordinates() + .map(|(x, y)| Shape::reposition((x, y), center)) + } + fn coordinates(&self) -> impl Iterator { let (mid_x, mid_y) = self.center(); // Loop over outer vec (i) @@ -771,45 +794,57 @@ fn update_position( // TODO: Move to event fn update_shape_blocks( query: Query< - (Entity, &Shape, &GridPosition), + (Entity, &ShapeLayout, &GridPosition), Or<( - Added, - Changed, - Added, + Changed, Changed, )>, >, - mut blocks: Query<&mut GridPosition, (With, Without)>, - mut commands: Commands, - visuals: Res, + mut blocks: Query<&mut GridPosition, (With, Without)>, ) { - query.iter().for_each(|(e, s, center)| { - debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii()); + query.iter().for_each(|(e, sl, center)| { + debug!("Setting piece: {e:?} {center:?}\n{}", sl.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, TETRIS)) - .observe(movement); - }); - }); - } else { - let mut p = s.coordinates(center); - blocks.iter_mut().for_each(|mut gp| { - *gp = p.next().unwrap().unwrap(); - }); - } + debug_assert!(!blocks.is_empty()); + + let mut p = sl.coordinates_at(center); + blocks.iter_mut().for_each(|mut gp| { + *gp = p.next().unwrap().unwrap(); + }); }); } +fn on_add_shape_layout( + trigger: On, + visuals: Res, + query: Query<(Entity, &Shape, &ShapeLayout, &GridPosition)>, + mut commands: Commands, +) { + let (e, s, sl, center) = query.get(trigger.entity).unwrap(); + let mesh = Mesh2d(visuals.mesh.clone()); + let mat = match s { + Shape::O => &visuals.material_o, + Shape::T => &visuals.material_t, + Shape::L => &visuals.material_l, + Shape::J => &visuals.material_j, + Shape::S => &visuals.material_s, + Shape::Z => &visuals.material_z, + Shape::I => &visuals.material_i, + }; + commands + .entity(e) + .with_related_entities::(|parent| { + sl.coordinates_at(center).for_each(|gp| { + parent + .spawn((mesh.clone(), MeshMaterial2d(mat.clone()), gp.unwrap(), Block, TETRIS)) + .observe(movement); + }); + }); +} + fn kb_input( mut events: MessageReader, - mut query: Query<(Entity, &mut Shape)>, + mut query: Query>, curr: Res>, mut next: ResMut>, mut commands: Commands, @@ -819,7 +854,7 @@ fn kb_input( key_code, state, .. }| { if let ButtonState::Pressed = state { - query.iter_mut().for_each(|(e, mut s)| { + query.iter_mut().for_each(|e| { match key_code { // Up arrow should rotate if in falling mode // Only move up if in falling::off mode @@ -860,13 +895,6 @@ fn kb_input( GameState::Falling => GameState::Pause, GameState::Pause => GameState::Falling, }), - 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(), _ => (), } }); @@ -885,29 +913,17 @@ fn falling(mut shape: Query>, mut commands: Commands) { }); } -// Run condition that returns `true` every `n` seconds -// TODO: Update a resource with the current tick -fn clock_cycle(n: f32) -> impl FnMut(Res