diff --git a/CHANGELOG b/CHANGELOG index f3d146f..e9c0cc7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,4 +21,11 @@ Gamepads auto-map to the first player that doesn't have a GamepadId mapping to i ######### Renamed project: Rustrisn-t -> Tetrisn-t. Improved menu slightly. -Various quality-of-life changes. \ No newline at end of file +Various quality-of-life changes. + +######### +# 0.1.1 # +######### +Added active piece tile highlights +Added line clear animation using tile highlights +Made it easier to quit to menu diff --git a/src/game.rs b/src/game.rs index 27e500b..0d08190 100644 --- a/src/game.rs +++ b/src/game.rs @@ -132,6 +132,8 @@ pub struct Game { tile_size: f32, batch_empty_tile: spritebatch::SpriteBatch, batch_highlight_active_tile: spritebatch::SpriteBatch, + batch_highlight_clearing_standard_tile: spritebatch::SpriteBatch, + batch_highlight_clearing_tetrisnt_tile: spritebatch::SpriteBatch, vec_batch_player_piece: Vec, vec_batch_next_piece: Vec, game_info_text: Text, @@ -250,7 +252,7 @@ impl Game { .scale(Scale::uniform(LITTLE_TEXT_SCALE)), ); let pause_text = Text::new( - TextFragment::new("PAUSED\n\nDown + RotateCw + RotateCcw + ESC/Start to quit") + TextFragment::new("PAUSED\n\nDown + ESC/Start to quit") .color(graphics::WHITE) .scale(Scale::uniform(LITTLE_TEXT_SCALE)), ); @@ -279,6 +281,12 @@ impl Game { batch_highlight_active_tile: spritebatch::SpriteBatch::new( TileGraphic::new_active_highlight(ctx).image, ), + batch_highlight_clearing_standard_tile: spritebatch::SpriteBatch::new( + TileGraphic::new_clear_standard_highlight(ctx).image, + ), + batch_highlight_clearing_tetrisnt_tile: spritebatch::SpriteBatch::new( + TileGraphic::new_clear_tetrisnt_highlight(ctx).image, + ), vec_batch_player_piece, vec_batch_next_piece, game_info_text, @@ -312,11 +320,7 @@ impl Game { } else { for player in &mut self.vec_players { // should we quit to main menu? - if player.input.keydown_down.0 - && player.input.keydown_rotate_ccw.0 - && player.input.keydown_rotate_cw.0 - && player.input.keydown_start.1 - { + if player.input.keydown_down.0 && player.input.keydown_start.1 { return ProgramState::Menu; } // should we resume? @@ -671,6 +675,64 @@ impl Game { } } } + // line clear highlights + for full_line in self.board.vec_full_lines.iter() { + if full_line.lines_cleared_together < 4 { + // standard clear animation + let y = (full_line.row - BOARD_HEIGHT_BUFFER_U) as usize; + let board_max_index_remainder_2 = (self.board.width - 1) % 2; + // go from the middle to the outside and reach the end right before full_line.clear_delay reaches 0 + for x in (self.board.width / 2)..self.board.width { + let highlight_pos_right = graphics::DrawParam::new().dest(Point2::new( + x as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + y as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + )); + let highlight_pos_left = graphics::DrawParam::new().dest(Point2::new( + (self.board.width as f32 - (x + board_max_index_remainder_2) as f32) + * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + y as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + )); + + self.batch_highlight_clearing_standard_tile + .add(highlight_pos_right); + self.batch_highlight_clearing_standard_tile + .add(highlight_pos_left); + + if ((x as f32) / (self.board.width as f32) - 0.5) * 2.0 + > 1.0 - (full_line.clear_delay as f32 / CLEAR_DELAY as f32) + { + break; + } + } + } else { + // tetrisnt clear animation + let y = (full_line.row - BOARD_HEIGHT_BUFFER_U) as usize; + let board_max_index_remainder_2 = (self.board.width - 1) % 2; + // go from the middle to the outside and reach the end right before full_line.clear_delay reaches 0 + for x in (self.board.width / 2)..self.board.width { + let highlight_pos_right = graphics::DrawParam::new().dest(Point2::new( + x as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + y as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + )); + let highlight_pos_left = graphics::DrawParam::new().dest(Point2::new( + (self.board.width as f32 - (x + board_max_index_remainder_2) as f32) + * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + y as f32 * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as f32, + )); + + self.batch_highlight_clearing_tetrisnt_tile + .add(highlight_pos_right); + self.batch_highlight_clearing_tetrisnt_tile + .add(highlight_pos_left); + + if ((x as f32) / (self.board.width as f32) - 0.5) * 2.0 + > 1.0 - (full_line.clear_delay as f32 / CLEAR_DELAY as f32) + { + break; + } + } + } + } // next pieces for player in &mut self.vec_players { if player.redraw_next_piece_flag { @@ -726,7 +788,7 @@ impl Game { ) .unwrap(); } - // highlights + // active tile highlights graphics::draw( ctx, &self.batch_highlight_active_tile, @@ -738,6 +800,30 @@ impl Game { .scale(Vector2::new(scaled_tile_size, scaled_tile_size)), ) .unwrap(); + // clearing tile standard highlights + graphics::draw( + ctx, + &self.batch_highlight_clearing_standard_tile, + DrawParam::new() + .dest(Point2::new( + board_top_left_corner, + NON_BOARD_SPACE_U as f32 * self.tile_size, + )) + .scale(Vector2::new(scaled_tile_size, scaled_tile_size)), + ) + .unwrap(); + // clearing tile tetrisnt highlights + graphics::draw( + ctx, + &self.batch_highlight_clearing_tetrisnt_tile, + DrawParam::new() + .dest(Point2::new( + board_top_left_corner, + NON_BOARD_SPACE_U as f32 * self.tile_size, + )) + .scale(Vector2::new(scaled_tile_size, scaled_tile_size)), + ) + .unwrap(); // next piece tiles for player in self.vec_players.iter() { graphics::draw( @@ -767,8 +853,12 @@ impl Game { for player in 0..self.num_players { self.vec_batch_player_piece[player as usize].clear(); } - // clear highlight sprite batch + // clear highlight active tile sprite batch self.batch_highlight_active_tile.clear(); + // clear highlight clearing tile standard sprite batch + self.batch_highlight_clearing_standard_tile.clear(); + // clear highlight clearing tile tetrisnt sprite batch + self.batch_highlight_clearing_tetrisnt_tile.clear(); } } diff --git a/src/game/board.rs b/src/game/board.rs index d2f2676..2aae505 100644 --- a/src/game/board.rs +++ b/src/game/board.rs @@ -13,7 +13,7 @@ pub struct Board { pub height: u8, pub matrix: Vec>, pub vec_active_piece: Vec, - vec_full_lines: Vec, + pub vec_full_lines: Vec, } // example Board coordinates system (2 width, 2 height) @@ -26,13 +26,23 @@ impl Board { for _ in 0..num_players { vec_active_piece.push(Piece::new(Shapes::None)); } + let matrix = vec![ + vec![Tile::default(); board_width as usize]; + (board_height + BOARD_HEIGHT_BUFFER_U) as usize + ]; + + // DEBUG TILES ADDED + // let mut matrix = vec![vec![Tile::new_empty(); board_width as usize]; (board_height + BOARD_HEIGHT_BUFFER_U) as usize]; + // for x in 0..(board_width - 1) { + // for y in (board_height + BOARD_HEIGHT_BUFFER_U - 8)..(board_height + BOARD_HEIGHT_BUFFER_U) { + // matrix[y as usize][x as usize] = Tile::new(false, false, 0u8); + // } + // } + Self { width: board_width, height: board_height, - matrix: vec![ - vec![Tile::new_empty(); board_width as usize]; - (board_height + BOARD_HEIGHT_BUFFER_U) as usize - ], + matrix, vec_active_piece, vec_full_lines: vec![], } @@ -45,7 +55,7 @@ impl Board { .take(4) { if position != &(0xffu8, 0xffu8) { - self.matrix[position.0 as usize][position.1 as usize] = Tile::new_empty(); + self.matrix[position.0 as usize][position.1 as usize] = Tile::default(); } else { println!("[!] tried to emptify piece that contained position (0xffu8, 0xffu8)"); } @@ -123,18 +133,24 @@ impl Board { if movement == Movement::Down && self.should_lock(player) { // lock piece and push any full lines to vec_full_lines self.vec_active_piece[player as usize].shape = Shapes::None; - let mut is_full_line = false; + let mut full_line_rows: Vec = Vec::with_capacity(4); for row in &self.lock_piece(player) { if self.is_row_full(*row) { - is_full_line = true; - self.vec_full_lines.push(FullLine::new(*row, player)); + full_line_rows.push(*row); } } - if is_full_line { + if !full_line_rows.is_empty() { + for row in full_line_rows.iter() { + self.vec_full_lines.push(FullLine::new( + *row, + full_line_rows.len() as u8, + player, + )); + } self.vec_full_lines.sort(); } - return (false, is_full_line); + return (false, !full_line_rows.is_empty()); } return (false, false); @@ -314,7 +330,7 @@ impl Board { self.matrix .remove(self.vec_full_lines[index - indices_destroyed].row as usize); self.matrix - .insert(0, vec![Tile::new_empty(); self.width as usize]); + .insert(0, vec![Tile::default(); self.width as usize]); self.vec_full_lines.remove(index - indices_destroyed); indices_destroyed += 1; // now is when we step backwards through the self.vec_full_lines vector, @@ -342,17 +358,19 @@ impl Board { } #[derive(Ord, Eq, PartialOrd, PartialEq)] -struct FullLine { +pub struct FullLine { pub row: u8, + pub lines_cleared_together: u8, pub player: u8, pub clear_delay: i8, pub remove_flag: bool, } impl FullLine { - pub fn new(row: u8, player: u8) -> Self { + pub fn new(row: u8, lines_cleared_together: u8, player: u8) -> Self { Self { row, + lines_cleared_together, player, clear_delay: CLEAR_DELAY, remove_flag: false, diff --git a/src/game/tile.rs b/src/game/tile.rs index cfb35aa..b4eefed 100644 --- a/src/game/tile.rs +++ b/src/game/tile.rs @@ -30,7 +30,7 @@ const PLAYER_TILE_BRIGHTEN: [f32; 3] = [0.40, 0.25, 0.10]; // [-][-][-][2][2][-][-][-] // [-][-][-][-][-][-][-][-] -// this one is actually opacity, since it's drawn over the top of a player's active piece's tiles +// this one is actually opacity out of 0xff, since it's drawn over the top of a player's active piece's tiles const PLAYER_TILE_ACTIVE_HIGHLIGHT: [u8; 3] = [0x50, 0x09, 0x00]; // [0][1][1][1][1][1][1][0] @@ -42,6 +42,30 @@ const PLAYER_TILE_ACTIVE_HIGHLIGHT: [u8; 3] = [0x50, 0x09, 0x00]; // [1][1][1][2][2][1][1][1] // [0][1][1][1][1][1][1][0] +// this one is actually opacity out of 0xff, since it's drawn over the top of clearing tiles' sprites +const CLEARING_TILE_STANDARD_HIGHLIGHT: [u8; 3] = [0x60, 0x10, 0x05]; + +// [0][1][1][1][1][1][1][0] +// [1][1][1][2][2][1][1][1] +// [1][1][2][2][2][2][1][1] +// [1][2][2][2][2][2][2][1] +// [1][2][2][2][2][2][2][1] +// [1][1][2][2][2][2][1][1] +// [1][1][1][2][2][1][1][1] +// [0][1][1][1][1][1][1][0] + +// this one is actually opacity out of 0xff, since it's drawn over the top of clearing tiles' sprites (when 4 lines are cleared at once) +const CLEARING_TILE_TETRISNT_HIGHLIGHT: [u8; 3] = [0xa0, 0x30, 0x10]; + +// [0][1][1][1][1][1][1][0] +// [1][1][1][2][2][1][1][1] +// [1][1][2][2][2][2][1][1] +// [1][2][2][2][2][2][2][1] +// [1][2][2][2][2][2][2][1] +// [1][1][2][2][2][2][1][1] +// [1][1][1][2][2][1][1][1] +// [0][1][1][1][1][1][1][0] + // defined player colors, otherwise it uses a "random" but consistent color based on BASE_PLAYER_COLOR const NUM_PLAYERCOLORS: u8 = 7; const PLAYER_RGBA: [(u8, u8, u8, u8); NUM_PLAYERCOLORS as usize] = [ @@ -71,8 +95,10 @@ impl Tile { player, } } +} - pub fn new_empty() -> Self { +impl Default for Tile { + fn default() -> Self { Self { empty: true, active: false, @@ -392,6 +418,144 @@ impl TileGraphic { } } + pub fn new_clear_standard_highlight(ctx: &mut Context) -> Self { + // create a buffer of (u8, u8, u8, u8), because rgba, big enough to hold each pixel + let mut pixel_color_buf: [(u8, u8, u8, u8); + NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize] = + [WHITE; + NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize]; + + // corners (0) + for x in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + for y in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[0]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[0]; + } + } + + // edges (1) + for x in 1..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1 { + for y in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[1]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[1]; + } + } + + // around the corners (1) + for x in &[ + 1, + 2, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 3, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2, + ] { + for y in &[1, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[1]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[1]; + } + } + + // inside (2) + for x in 3..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 3 { + for y in &[1, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[2]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[2]; + } + } + for x in 2..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2 { + for y in 2..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2 { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_STANDARD_HIGHLIGHT[2]; + } + } + + Self { + image: graphics::Image::from_rgba8( + ctx, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC, + &TileGraphic::pack_color_buf(&pixel_color_buf), + ) + .expect("Failed to create tile clearing standard highlight image"), + } + } + + pub fn new_clear_tetrisnt_highlight(ctx: &mut Context) -> Self { + // create a buffer of (u8, u8, u8, u8), because rgba, big enough to hold each pixel + let mut pixel_color_buf: [(u8, u8, u8, u8); + NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize] = + [WHITE; + NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize * NUM_PIXEL_ROWS_PER_TILEGRAPHIC as usize]; + + // corners (0) + for x in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + for y in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[0]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[0]; + } + } + + // edges (1) + for x in 1..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1 { + for y in &[0, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 1] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[1]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[1]; + } + } + + // around the corners (1) + for x in &[ + 1, + 2, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 3, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2, + ] { + for y in &[1, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[1]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[1]; + } + } + + // inside (2) + for x in 3..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 3 { + for y in &[1, NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2] { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[2]; + pixel_color_buf[(y + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * x) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[2]; + } + } + for x in 2..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2 { + for y in 2..NUM_PIXEL_ROWS_PER_TILEGRAPHIC - 2 { + pixel_color_buf[(x + NUM_PIXEL_ROWS_PER_TILEGRAPHIC * y) as usize].3 = + CLEARING_TILE_TETRISNT_HIGHLIGHT[2]; + } + } + + Self { + image: graphics::Image::from_rgba8( + ctx, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC, + NUM_PIXEL_ROWS_PER_TILEGRAPHIC, + &TileGraphic::pack_color_buf(&pixel_color_buf), + ) + .expect("Failed to create tile clearing tetrisnt highlight image"), + } + } + pub fn get_size(ctx: &mut Context, board_width: u8, board_height: u8) -> f32 { std::cmp::min( graphics::size(ctx).1 as u32 / board_height as u32,