diff --git a/Cargo.lock b/Cargo.lock index 2bf9502..3523a66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,6 +610,9 @@ dependencies = [ "rand", "rodio", "sdl2", + "serde", + "toml", + "xdg", ] [[package]] @@ -669,6 +672,35 @@ dependencies = [ "version-compare", ] +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -770,11 +802,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -783,6 +830,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -1094,6 +1143,12 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 2809f90..9dd3450 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,9 @@ edition = "2021" [dependencies] rand = "0.9.0" rodio = "0.20.1" +serde = { version = "1.0.218", features = ["serde_derive"] } +toml = "0.8.20" +xdg = "2.5.2" [dependencies.sdl2] version = "0.37.0" diff --git a/src/gui/sdl.rs b/src/gui/sdl.rs index 39337d2..1c816ed 100644 --- a/src/gui/sdl.rs +++ b/src/gui/sdl.rs @@ -6,6 +6,8 @@ use sdl2::pixels::Color; use sdl2::rect::Rect; use sdl2::render::{Texture, TextureCreator, WindowCanvas}; use sdl2::ttf::Sdl2TtfContext; +use serde::{Deserialize, Serialize}; +use std::fs; use std::time::Duration; use super::audio::{self}; @@ -111,11 +113,228 @@ impl UiCtx for SdlUiCtx { } } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "key")] +enum Key { + Zero, + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + Up, + Down, + Left, + Right, + Enter, + Backspace, + Space, +} + +impl Key { + fn from_sdl_keycode(keycode: Keycode) -> Option { + let v = match keycode { + Keycode::Num0 | Keycode::Kp0 => Key::Zero, + Keycode::Num1 | Keycode::Kp1 => Key::One, + Keycode::Num2 | Keycode::Kp2 => Key::Two, + Keycode::Num3 | Keycode::Kp3 => Key::Three, + Keycode::Num4 | Keycode::Kp4 => Key::Four, + Keycode::Num5 | Keycode::Kp5 => Key::Five, + Keycode::Num6 | Keycode::Kp6 => Key::Six, + Keycode::Num7 | Keycode::Kp7 => Key::Seven, + Keycode::Num8 | Keycode::Kp8 => Key::Eight, + Keycode::Num9 | Keycode::Kp9 => Key::Nine, + Keycode::A => Key::A, + Keycode::B => Key::B, + Keycode::C => Key::C, + Keycode::D => Key::D, + Keycode::E => Key::E, + Keycode::F => Key::F, + Keycode::G => Key::G, + Keycode::H => Key::H, + Keycode::I => Key::I, + Keycode::J => Key::J, + Keycode::K => Key::K, + Keycode::L => Key::L, + Keycode::M => Key::M, + Keycode::N => Key::N, + Keycode::O => Key::O, + Keycode::P => Key::P, + Keycode::Q => Key::Q, + Keycode::R => Key::R, + Keycode::S => Key::S, + Keycode::T => Key::T, + Keycode::U => Key::U, + Keycode::V => Key::V, + Keycode::W => Key::W, + Keycode::X => Key::X, + Keycode::Y => Key::Y, + Keycode::Z => Key::Z, + Keycode::Up => Key::Up, + Keycode::Down => Key::Down, + Keycode::Left => Key::Left, + Keycode::Right => Key::Right, + Keycode::Return => Key::Enter, + Keycode::Backspace => Key::Backspace, + Keycode::Space => Key::Space, + _ => return None, + }; + Some(v) + } +} + +impl std::fmt::Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let val = match self { + Key::Zero => "0", + Key::One => "1", + Key::Two => "2", + Key::Three => "3", + Key::Four => "4", + Key::Five => "5", + Key::Six => "6", + Key::Seven => "7", + Key::Eight => "8", + Key::Nine => "9", + Key::A => "A", + Key::B => "B", + Key::C => "C", + Key::D => "D", + Key::E => "E", + Key::F => "F", + Key::G => "G", + Key::H => "H", + Key::I => "I", + Key::J => "J", + Key::K => "K", + Key::L => "L", + Key::M => "M", + Key::N => "N", + Key::O => "O", + Key::P => "P", + Key::Q => "Q", + Key::R => "R", + Key::S => "S", + Key::T => "T", + Key::U => "U", + Key::V => "V", + Key::W => "W", + Key::X => "X", + Key::Y => "Y", + Key::Z => "Z", + Key::Up => "Up", + Key::Down => "Down", + Key::Left => "Left", + Key::Right => "Right", + Key::Enter => "Enter", + Key::Backspace => "Backspace", + Key::Space => "Space", + }; + write!(f, "{val}") + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct Config { + reimtris1_feature_parity: bool, + restart: Vec, + left: Vec, + right: Vec, + rotate_cw: Vec, + rotate_ccw: Vec, + soft_drop: Vec, + hard_drop: Vec, + swap: Vec, + pause: Vec, + toggle_mute: Vec, +} + +impl Default for Config { + fn default() -> Self { + Self { + reimtris1_feature_parity: false, + restart: vec![Key::Enter, Key::Space], + left: vec![Key::Left], + right: vec![Key::Right], + rotate_cw: vec![Key::X], + rotate_ccw: vec![Key::Z], + soft_drop: vec![Key::Down], + hard_drop: vec![Key::Space], + swap: vec![Key::C], + pause: vec![Key::P], + toggle_mute: vec![Key::M], + } + } +} + +fn config_from_file>(path: P) -> Result { + let Some(config) = fs::read_to_string(path.as_ref()).ok() else { + let config = Config::default(); + { + println!("could not get config! attempting to create default..."); + let config = toml::to_string(&config).map_err(|err| err.to_string())?; + fs::write(path, config).map_err(|err| err.to_string())?; + } + return Ok(config); + }; + let Some(config) = toml::from_str(&config).ok() else { + println!("womp womp, config contains an invalid config, attempting to reset to default..."); + let config = Config::default(); + { + let config = toml::to_string(&config).map_err(|err| err.to_string())?; + fs::write(path, config).map_err(|err| err.to_string())?; + } + return Ok(config); + }; + Ok(config) +} + pub fn start_game() -> Result<(), String> { let mut game = Game::new(); let mut actions = ActionsHeld::new(); let mut paused = false; + let config = { + let base = xdg::BaseDirectories::new().map_err(|err| err.to_string())?; + let path = base + .place_config_file("reimtris2/config.toml") + .map_err(|err| err.to_string())?; + let config = config_from_file(path)?; + config + }; + + const FONT: &'static str = "resources/josenfin_sans_regular.ttf"; + let audio_thread = audio::audio_thread(); let sdl_context = sdl2::init()?; @@ -125,6 +344,7 @@ pub fn start_game() -> Result<(), String> { let window = video_subsystem .window("reimtris2", 1000, 800) .resizable() + .maximized() .position_centered() .build() .unwrap(); @@ -144,50 +364,77 @@ pub fn start_game() -> Result<(), String> { keycode: Some(Keycode::Escape), .. } => break 'running Ok(()), + Event::MouseMotion { .. } => { + if config.reimtris1_feature_parity { + break 'running Ok(()); + } + } Event::KeyDown { keycode: Some(keycode), .. } => { - let keycode = match keycode { - Keycode::Return if !paused && game.game_over => { - game = Game::new(); - continue; - } - Keycode::M => { - audio_thread.send(audio::Command::ToggleMuted).unwrap(); - continue; - } - - Keycode::P => { - paused = !paused; - continue; - } - Keycode::Left | Keycode::A => Action::Left, - Keycode::Right | Keycode::D => Action::Right, - Keycode::Down | Keycode::S => Action::SoftDrop, - Keycode::Space => Action::HardDrop, - Keycode::Z => Action::RotateCcw, - Keycode::X => Action::RotateCw, - Keycode::C => Action::Swap, - _ => continue, + let Some(key) = Key::from_sdl_keycode(keycode) else { + continue; }; - actions.insert(keycode, game.ticks); + if config.pause.contains(&key) { + paused = !paused; + }; + if config.restart.contains(&key) && !paused && game.game_over { + game = Game::new(); + } + if config.toggle_mute.contains(&key) { + audio_thread.send(audio::Command::ToggleMuted).unwrap(); + } + if config.left.contains(&key) { + actions.insert(Action::Left, game.ticks); + } + if config.right.contains(&key) { + actions.insert(Action::Right, game.ticks); + } + if config.soft_drop.contains(&key) { + actions.insert(Action::SoftDrop, game.ticks); + } + if config.hard_drop.contains(&key) { + actions.insert(Action::HardDrop, game.ticks); + } + if config.rotate_cw.contains(&key) { + actions.insert(Action::RotateCw, game.ticks); + } + if config.rotate_ccw.contains(&key) { + actions.insert(Action::RotateCcw, game.ticks); + } + if config.swap.contains(&key) { + actions.insert(Action::Swap, game.ticks); + } } Event::KeyUp { keycode: Some(keycode), .. } => { - let keycode = match keycode { - Keycode::Left | Keycode::A => Action::Left, - Keycode::Right | Keycode::D => Action::Right, - Keycode::Down | Keycode::S => Action::SoftDrop, - Keycode::Space => Action::HardDrop, - Keycode::Z => Action::RotateCcw, - Keycode::X => Action::RotateCw, - Keycode::C => Action::Swap, - _ => continue, + let Some(key) = Key::from_sdl_keycode(keycode) else { + continue; }; - actions.remove(&keycode); + if config.left.contains(&key) { + actions.remove(&Action::Left); + } + if config.right.contains(&key) { + actions.remove(&Action::Right); + } + if config.soft_drop.contains(&key) { + actions.remove(&Action::SoftDrop); + } + if config.hard_drop.contains(&key) { + actions.remove(&Action::HardDrop); + } + if config.rotate_cw.contains(&key) { + actions.remove(&Action::RotateCw); + } + if config.rotate_ccw.contains(&key) { + actions.remove(&Action::RotateCcw); + } + if config.swap.contains(&key) { + actions.remove(&Action::Swap); + } } _ => {} } @@ -195,17 +442,28 @@ pub fn start_game() -> Result<(), String> { ctx.draw_board(&game.board, &game.current_tetromino)?; ctx.draw_bag(&game.held_tetromino, &game.next_tetrominos)?; + ctx.draw_score(FONT, &game.score)?; if paused { - ctx.draw_important_text( - "resources/josenfin_sans_regular.ttf", - "game paused o_o... press [p] to unpause !!", - )?; + let keys = config + .pause + .iter() + .map(|v| v.to_string().to_lowercase()) + .collect::>() + .join(" | "); + let paused = format!("game paused o_o... press [{keys}] to unpause !!"); + + ctx.draw_important_text(FONT, paused)?; } else if game.game_over { - ctx.draw_important_text( - "resources/josenfin_sans_regular.ttf", - "game over T_T... press [enter] 2 restart :D", - )?; + let keys = config + .restart + .iter() + .map(|v| v.to_string().to_lowercase()) + .collect::>() + .join(" | "); + + let game_over = format!("game over T_T... press [{keys}] 2 restart :D"); + ctx.draw_important_text(FONT, game_over)?; } else { let effects = game.step(&actions); effects.into_iter().for_each(|effect| { diff --git a/src/gui/ui.rs b/src/gui/ui.rs index 84dcf0e..ab67d18 100644 --- a/src/gui/ui.rs +++ b/src/gui/ui.rs @@ -1,6 +1,6 @@ use crate::{ board::Board, - game::CurrentTetromino, + game::{CurrentTetromino, Score}, tetromino::{Direction, Tetromino}, }; @@ -180,6 +180,30 @@ pub trait GameUiCtx: UiCtx { Ok(()) } + fn draw_score>(&mut self, font: P, score: &Score) -> Result<(), Err> { + let (win_width, win_height) = self.window_size()?; + let board_width = self.tile_size() * Board::WIDTH as i32; + let board_height = self.tile_size() * Board::HEIGHT as i32; + let x = center(board_width, win_width) + board_width + self.tile_size(); + let y = center(board_height, win_height) + self.tile_size(); + + let level = format!("level: {}", score.level); + let lines = format!("lines: {}", score.lines); + let points = format!("points: {}", score.points); + + let level_size = self.text_size(font.as_ref(), &level)?; + let lines_size = self.text_size(font.as_ref(), &lines)?; + let points_size = self.text_size(font.as_ref(), &points)?; + + self.fill_text(font.as_ref(), level, x, y, level_size.0, level_size.1)?; + let y = y + level_size.1 + self.tile_size(); + self.fill_text(font.as_ref(), lines, x, y, lines_size.0, lines_size.1)?; + let y = y + lines_size.1 + self.tile_size(); + self.fill_text(font.as_ref(), points, x, y, points_size.0, points_size.1)?; + + Ok(()) + } + fn draw_board(&mut self, board: &Board, current: &CurrentTetromino) -> Result<(), Err> { let (win_width, win_height) = self.window_size()?; self.outline_rect(