diff --git a/Cargo.toml b/Cargo.toml index 7724628..c07b322 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,14 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ggez = "^0.5.1" -toml = "^0.5.6" +env_logger = "0.8.3" +image = "0.23.14" +log = "0.4.14" +pixels = "0.3.0" +rodio = "^0.11.0" +rodio-xm = { git = "https://tinybird.dev/max/rodio-xm/", branch = "master" } serde = "^1.0.114" serde_derive = "^1.0.114" +toml = "^0.5.6" +winit = "0.24.0" +winit_input_helper = "0.9.0" diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..77f1d56 --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +# to do + +## game data structure + +### parser + +* ~move tests into their respective modules where appropriate~ + +## players + +* ~graphics~ + * re-use player avatar drawing function as generic image drawing function + * text (how?) + * support older graphics adaptors + +### linux + +* ~get working~ + +### windows + +* ~try to compile~ + * does not work on my acer laptop (2012) + * does not work on my windows 8 VM + * works on my computer via wine! + +### future: arm/raspberry pi? + +### web + +will need: + +* base64 decoding +* zip parsing +* webgl or something? +* audio?? + +## editor + +* build something in egui +* can we do a web version that works with zip files? + * investigate zip compression/decompression in rust diff --git a/src/bin/player.rs b/src/bin/player.rs index 4ca1bfd..22fddd1 100644 --- a/src/bin/player.rs +++ b/src/bin/player.rs @@ -1,302 +1,301 @@ -use ggez; +#[windows_subsystem = "windows"] -// Next we need to actually `use` the pieces of ggez that we are going -// to need frequently. -use ggez::event::{KeyCode, KeyMods, EventsLoop}; -use ggez::{event, graphics, Context, GameResult}; +use log::error; +use pixels::{Error, SurfaceTexture, PixelsBuilder}; +use pixels::wgpu::BackendBit; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit_input_helper::WinitInputHelper; +use std::collections::HashMap; -// We'll bring in some things from `std` to help us in the future. -use std::time::{Duration, Instant}; +#[derive(Clone, Debug)] +struct Image { + pixels: [u8; 64] +} -use ggez::graphics::{Rect}; -use ggez::conf::FullscreenType; +struct Game { + width: usize, + height: usize, + player_position: (u8, u8), + player_avatar: Image, + palette: [[u8; 4]; 4], + current_music: Option, + music: HashMap, +} -// The first thing we want to do is set up some constants that will help us out later. - -// Here we define the size of our game board in terms of how many grid -// cells it will take up. We choose to make a 30 x 20 game board. -const GRID_SIZE: (u8, u8) = (16, 9); -// dimension, i.e. 8×8 (square) -const GRID_CELL_SIZE: u8 = 8; - -const UPDATES_PER_SECOND: f32 = 0.4; -// And we get the milliseconds of delay that this update rate corresponds to. -const MILLIS_PER_UPDATE: u64 = (1.0 / UPDATES_PER_SECOND * 1000.0) as u64; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -struct GridPosition { x: u8, y: u8 } - -impl GridPosition { - /// We make a standard helper function so that we can create a new `GridPosition` - /// more easily. - pub fn new(x: u8, y: u8) -> Self { - GridPosition { x, y } - } - - /// We'll make another helper function that takes one grid position and returns a new one after - /// making one move in the direction of `dir`. We use our `SignedModulo` trait - /// above, which is now implemented on `i16` because it satisfies the trait bounds, - /// to automatically wrap around within our grid size if the move would have otherwise - /// moved us off the board to the top, bottom, left, or right. - pub fn new_from_move(pos: GridPosition, dir: Direction) -> Self { - match dir { - Direction::Up => GridPosition::new(pos.x, pos.y - 1), - Direction::Down => GridPosition::new(pos.x, pos.y + 1), - Direction::Left => GridPosition::new(pos.x - 1, pos.y), - Direction::Right => GridPosition::new(pos.x + 1, pos.y), +impl Game { + fn draw(&self, screen: &mut [u8]) { + // clear screen + for pixel in screen.chunks_exact_mut(4) { + pixel.copy_from_slice(&self.palette[0]); } - } -} -/// We implement the `From` trait, which in this case allows us to convert easily between -/// a GridPosition and a ggez `graphics::Rect` which fills that grid cell. -/// Now we can just call `.into()` on a `GridPosition` where we want a -/// `Rect` that represents that grid cell. -impl From for graphics::Rect { - fn from(pos: GridPosition) -> Self { - graphics::Rect::new_i32( - pos.x as i32 * GRID_CELL_SIZE as i32, - pos.y as i32 * GRID_CELL_SIZE as i32, - GRID_CELL_SIZE as i32, - GRID_CELL_SIZE as i32, - ) - } -} + let (player_x, player_y) = self.player_position; -impl From<(u8, u8)> for GridPosition { - fn from(pos: (u8, u8)) -> Self { - GridPosition { x: pos.0, y: pos.1 } - } -} + // each row of player avatar + for (tile_y, row) in self.player_avatar.pixels.chunks(8).enumerate() { + for (tile_x, pixel) in row.iter().enumerate() { + let colour = self.palette[*pixel as usize]; -/// Next we create an enum that will represent all the possible -/// directions that our snake could move. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Direction { - Up, - Down, - Left, - Right, -} - -impl Direction { - /// We also create a helper function that will let us convert between a - /// `ggez` `Keycode` and the `Direction` that it represents. Of course, - /// not every keycode represents a direction, so we return `None` if this - /// is the case. - pub fn from_keycode(key: KeyCode) -> Option { - match key { - KeyCode::Up => Some(Direction::Up), - KeyCode::Down => Some(Direction::Down), - KeyCode::Left => Some(Direction::Left), - KeyCode::Right => Some(Direction::Right), - _ => None, - } - } -} - -struct Avatar { - pos: GridPosition, -} - -impl Avatar { - pub fn new(pos: GridPosition) -> Self { - Avatar { - pos - } - } - - /// The main update function for our snake which gets called every time - /// we want to update the game state. - fn update(&mut self) { - - } - - /// Again, note that this approach to drawing is fine for the limited scope of this - /// example, but larger scale games will likely need a more optimized render path - /// using SpriteBatch or something similar that batches draw calls. - fn draw(&self, ctx: &mut Context, multiplier: &u8) -> GameResult<()> { - let dimension = (GRID_CELL_SIZE * multiplier) as f32; - - // And then we do the same for the head, instead making it fully red to distinguish it. - let rectangle = graphics::Mesh::new_rectangle( - ctx, - graphics::DrawMode::fill(), - ggez::graphics::Rect { - x: (self.pos.x as u16 * GRID_CELL_SIZE as u16 * *multiplier as u16) as f32, - y: (self.pos.y as u16 * GRID_CELL_SIZE as u16 * *multiplier as u16) as f32, - w: dimension, - h: dimension, - }, - [1.0, 0.5, 0.0, 1.0].into(), - )?; - - graphics::draw(ctx, &rectangle, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; - Ok(()) - } -} - -/// Now we have the heart of our game, the GameState. This struct -/// will implement ggez's `EventHandler` trait and will therefore drive -/// everything else that happens in our game. -struct GameState { - avatar: Avatar, - /// And we track the last time we updated so that we can limit - /// our update rate. - last_update: Instant, - /// integer multiples for scaling the display - size_multiplier: u8, - fullscreen: bool, - scene_width: u8, - scene_height: u8, -} - -impl GameState { - pub fn new() -> Self { - let avatar = Avatar { - pos: (GRID_SIZE.0 / 4, GRID_SIZE.1 / 2).into(), - }; - - GameState { - avatar, - last_update: Instant::now(), - size_multiplier: 4, - fullscreen: false, - scene_width: 16, - scene_height: 9 - } - } - - fn toggle_fullscreen(&mut self) { - self.fullscreen = !self.fullscreen; - } -} - -/// Now we implement EventHandler for GameState. This provides an interface -/// that ggez will call automatically when different events happen. -impl event::EventHandler for GameState { - /// Update will happen on every frame before it is drawn. This is where we update - /// our game state to react to whatever is happening in the game world. - fn update(&mut self, _ctx: &mut Context) -> GameResult { - // First we check to see if enough time has elapsed since our last update based on - // the update rate we defined at the top. - if Instant::now() - self.last_update >= Duration::from_millis(MILLIS_PER_UPDATE) { - self.avatar.update(); - - // If we updated, we set our last_update to be now - self.last_update = Instant::now(); - } - // Finally we return `Ok` to indicate we didn't run into any errors - Ok(()) - } - - /// draw is where we should actually render the game's current state. - fn draw(&mut self, ctx: &mut Context) -> GameResult { - graphics::clear(ctx, [0.1, 0.1, 0.1, 1.0].into()); - - // todo draw the whole game, not just the avatar - self.avatar.draw(ctx, &self.size_multiplier)?; - - // Finally we call graphics::present to cycle the GPU's framebuffer and display - // the new frame we just drew. - graphics::present(ctx)?; - - // We yield the current thread until the next update - ggez::timer::yield_now(); - - Ok(()) - } - - /// key_down_event gets fired when a key gets pressed. - fn key_down_event( - &mut self, - ctx: &mut Context, - keycode: KeyCode, - modifier: KeyMods, - _repeat: bool, - ) { - if let Some(dir) = Direction::from_keycode(keycode) { - match dir { - Direction::Up => { - if self.avatar.pos.y > 0 { - self.avatar.pos.y -= 1 - } - } - Direction::Right => { - if self.avatar.pos.x < GRID_SIZE.0 - 1 { - self.avatar.pos.x += 1 - } - } - Direction::Down => { - // if y is less than 8 it's ok to increment it - if self.avatar.pos.y < GRID_SIZE.1 - 1 { - self.avatar.pos.y += 1 - } - } - Direction::Left => { - if self.avatar.pos.x > 0 { - self.avatar.pos.x -= 1 - } + for (v, value) in colour.iter().enumerate() { + screen[ + ( + // player vertical offset (number of lines above) + (player_y as usize * 8 * (8 * self.width as usize)) + + + // player horizontal offset; number of pixels to the left + (player_x as usize * 8) + + + // tile vertical offset; number of lines within tile + (tile_y as usize * (8 * self.width as usize)) + + + (tile_x as usize) // tile horizontal offset + ) + * 4 // we're dealing with rgba values so multiply everything by 4 + + v // value offset: which of the rgba values? + ] = value.clone(); } } } - - // todo handle plus/minus keys - if keycode == KeyCode::Add { - self.size_multiplier += 1; - } else if keycode == KeyCode::Subtract && self.size_multiplier > 1 { - self.size_multiplier -= 1; - } - - if keycode == KeyCode::F11 || (modifier == KeyMods::ALT && keycode == KeyCode::Return) { - self.toggle_fullscreen(); - - if self.fullscreen { - graphics::set_fullscreen(ctx, FullscreenType::True).unwrap(); - } else { - graphics::set_fullscreen(ctx, FullscreenType::Windowed).unwrap(); - graphics::set_drawable_size( - ctx, - (self.scene_width * self.size_multiplier) as f32, - (self.scene_height * self.size_multiplier) as f32 - ).unwrap(); - } - } - - // todo change window size } + + // fn update(&self) { + // + // } } -fn window_setup(x: u8, y: u8, multiplier: u8) -> (Context, EventsLoop) { - let x = (x as u16 * GRID_CELL_SIZE as u16 * multiplier as u16) as f32; - let y = (y as u16 * GRID_CELL_SIZE as u16 * multiplier as u16) as f32; +fn main() -> Result<(), Error> { + env_logger::init(); - // Here we use a ContextBuilder to setup metadata about our game. First the title and author - let (ctx, events_loop) = ggez::ContextBuilder::new("snake", "Gray Olson") - // Next we set up the window. This title will be displayed in the title bar of the window. - .window_setup( - ggez::conf::WindowSetup::default().title("Write your game's title here") - ) - // Now we get to set the size of the window, which we use our SCREEN_SIZE constant from earlier to help with - .window_mode(ggez::conf::WindowMode::default().dimensions(x, y)) - // And finally we attempt to build the context and create the window. If it fails, we panic with the message - // "Failed to build ggez context" - .build() - .unwrap(); + let mut game = Game { + width: 16, + height: 9, + player_position: (8, 4), + player_avatar: Image { pixels: [ + 0,0,0,2,2,0,0,0, + 0,0,0,2,2,0,0,0, + 0,0,1,1,1,0,2,0, + 0,1,1,1,1,1,0,0, + 2,0,1,1,1,0,0,0, + 0,0,3,3,3,0,0,0, + 0,0,3,0,3,0,0,0, + 0,0,3,0,3,0,0,0, + ]}, + palette: [ + [0xff, 0x7f, 0x7f, 0xff], + [0xff, 0xb2, 0x7f, 0xff], + [0xff, 0xe9, 0x7f, 0xff], + [0x00, 0x7f, 0x7f, 0x46], + ], + current_music: None, + music: HashMap::new(), + }; - (ctx, events_loop) -} + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); -fn main() -> GameResult { - // Next we create a new instance of our GameState struct, which implements EventHandler - let state = &mut GameState::new(); - - let (mut ctx, mut events_loop) = window_setup( - GRID_SIZE.0, - GRID_SIZE.1, - state.size_multiplier + let (window, p_width, p_height, mut _hidpi_factor) = create_window( + "pixels test", + (game.width * 8) as f64, + (game.height * 8) as f64, + &event_loop ); - // And finally we actually run our game, passing in our context and state. - event::run(&mut ctx, &mut events_loop, state) + let surface_texture = SurfaceTexture::new(p_width, p_height, &window); + + let mut pixels = PixelsBuilder::new( + (game.width * 8) as u32, (game.height * 8) as u32, surface_texture + ) + .wgpu_backend(BackendBit::GL | BackendBit::PRIMARY) + .enable_vsync(false) + .build()?; + + let device = rodio::default_output_device().unwrap(); + + let source = rodio_xm::XMSource::from_bytes( + include_bytes!("../ninety degrees.xm") + ); + + let sink = rodio::Sink::new(&device); + sink.append(source); + sink.pause(); + + game.music.insert(":ninety degrees".into(), sink); + + let source = rodio_xm::XMSource::from_bytes( + include_bytes!("../orn_keygentheme2001.xm") + ); + + let sink = rodio::Sink::new(&device); + sink.append(source); + sink.pause(); + + game.music.insert("orn_keygentheme2001".into(), sink); + + game.current_music = None; + + event_loop.run(move |event, _, control_flow| { + // The one and only event that winit_input_helper doesn't have for us... + if let Event::RedrawRequested(_) = event { + game.draw(pixels.get_frame()); + + if pixels + .render() + .map_err(|e| error!("pixels.render() failed: {:?}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + } + + // For everything else, for let winit_input_helper collect events to build its state. + // It returns `true` when it is time to update our game state and request a redraw. + if input.update(&event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + + if input.key_pressed(VirtualKeyCode::M) { + // pause the current tune + if game.current_music.is_some() { + game.music.get(game.current_music.as_ref().unwrap()).unwrap().pause(); + } + + if game.current_music.is_none() || game.current_music.as_ref().unwrap() == "orn_keygentheme2001" { + // play the first tune + game.current_music = Some(":ninety degrees".into()); + game.music.get(game.current_music.as_ref().unwrap()).unwrap().play(); + } else { + // play the second tune + game.current_music = Some("orn_keygentheme2001".into()); + game.music.get(game.current_music.as_ref().unwrap()).unwrap().play(); + } + } + + if input.key_pressed(VirtualKeyCode::Left) { + let (x, y) = game.player_position; + if x > 0 { + game.player_position = (x - 1, y); + window.request_redraw(); + } + } + + if input.key_pressed(VirtualKeyCode::Right) { + let (x, y) = game.player_position; + if x < game.width as u8 - 1 { + game.player_position = (x + 1, y); + window.request_redraw(); + } + } + + if input.key_pressed(VirtualKeyCode::Up) { + let (x, y) = game.player_position; + if y > 0 { + game.player_position = (x, y - 1); + window.request_redraw(); + } + } + + if input.key_pressed(VirtualKeyCode::Down) { + let (x, y) = game.player_position; + if y < game.height as u8 - 1 { + game.player_position = (x, y + 1); + window.request_redraw(); + } + } + + // Adjust high DPI factor + if let Some(factor) = input.scale_factor_changed() { + _hidpi_factor = factor; + window.request_redraw(); + } + + // Resize the window + if let Some(size) = input.window_resized() { + pixels.resize_surface(size.width, size.height); + window.request_redraw(); + } + + *control_flow = ControlFlow::Wait; + } + }); +} + +#[cfg(not(target_os = "windows"))] +fn window_builder(title: &str, event_loop: &EventLoop<()>) -> winit::window::Window { + winit::window::WindowBuilder::new() + .with_visible(false) + .with_title(title).build(&event_loop).unwrap() +} + +#[cfg(target_os = "windows")] +fn window_builder(title: &str, event_loop: &EventLoop<()>) -> winit::window::Window { + use winit::platform::windows::WindowBuilderExtWindows; + + winit::window::WindowBuilder::new() + .with_drag_and_drop(false) + .with_visible(false) + .with_title(title).build(&event_loop).unwrap() +} + +/// Create a window for the game. +/// +/// Automatically scales the window to cover about 2/3 of the monitor height. +/// +/// # Returns +/// +/// Tuple of `(window, surface, width, height, hidpi_factor)` +/// `width` and `height` are in `PhysicalSize` units. +fn create_window( + title: &str, + width: f64, + height: f64, + event_loop: &EventLoop<()>, +) -> (winit::window::Window, u32, u32, f64) { + let window = window_builder(title, event_loop); + + let hidpi_factor = window.scale_factor(); + + // Get dimensions + + let (monitor_width, monitor_height) = { + if let Some(monitor) = window.current_monitor() { + let size = monitor.size().to_logical(hidpi_factor); + (size.width, size.height) + } else { + (width, height) + } + }; + + let scale = (monitor_height / height * 2.0 / 3.0).round().max(1.0); + + // Resize, center, and display the window + let min_size: winit::dpi::LogicalSize = + PhysicalSize::new(width, height).to_logical(hidpi_factor); + + let default_size = + LogicalSize::new(width * scale, height * scale); + + let center = LogicalPosition::new( + (monitor_width - width * scale) / 2.0, + (monitor_height - height * scale) / 2.0, + ); + + window.set_inner_size(default_size); + window.set_min_inner_size(Some(min_size)); + window.set_outer_position(center); + window.set_visible(true); + + let size = default_size.to_physical::(hidpi_factor); + + ( + window, + size.width.round() as u32, + size.height.round() as u32, + hidpi_factor, + ) } diff --git a/src/colour.rs b/src/colour.rs new file mode 100644 index 0000000..0e9f258 --- /dev/null +++ b/src/colour.rs @@ -0,0 +1,44 @@ +use serde_derive::{Serialize, Deserialize}; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Colour { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +impl Colour { + pub fn from(colours: Vec) -> Colour { + const ZERO: u8 = 0; + Colour { + red: *colours.get(0).unwrap_or(&ZERO), + green: *colours.get(1).unwrap_or(&ZERO), + blue: *colours.get(2).unwrap_or(&ZERO), + alpha: *colours.get(3).unwrap_or(&255), + } + } + + pub fn to_vec(&self) -> Vec { + vec![self.red, self.green, self.blue, self.alpha] + } +} + +#[cfg(test)] +mod test { + use crate::Colour; + + #[test] + fn test_colour_from_intermediate() { + let output = Colour::from(vec![64, 128, 192, 255]); + let expected = Colour { red: 64, green: 128, blue: 192, alpha: 255 }; + assert_eq!(output, expected); + } + + #[test] + fn test_colour_to_intermediate() { + let output = Colour { red: 64, green: 128, blue: 192, alpha: 255 }.to_vec(); + let expected = vec![64, 128, 192, 255]; + assert_eq!(output, expected); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c59b2be --- /dev/null +++ b/src/config.rs @@ -0,0 +1,49 @@ +use serde_derive::{Serialize, Deserialize}; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Config { + /// used in the window title bar + name: Option, + width: u8, + height: u8, + /// animation rate in milliseconds + tick: u64, + /// if this is not specified, the game will pick the first room it finds + starting_room: Option, + /// major / minor + version: (u8, u8), +} + +#[cfg(test)] +mod test { + use crate::Config; + + #[test] + fn test_config_from_toml() { + let output: Config = toml::from_str(include_str!("test-resources/basic/game.toml")).unwrap(); + let expected = Config { + name: Some("Write your game's title here".to_string()), + width: 16, + height: 9, + tick: 400, + starting_room: Some("example room".to_string()), + version: (0, 0) + }; + assert_eq!(output, expected); + } + + #[test] + fn test_config_to_toml() { + let output = toml::to_string(&Config { + name: Some("Write your game's title here".to_string()), + width: 16, + height: 9, + tick: 400, + starting_room: Some("example room".to_string()), + version: (0, 0) + }).unwrap(); + + let expected = include_str!("test-resources/basic/game.toml"); + assert_eq!(&output, expected); + } +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..7533e07 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,72 @@ +use serde_derive::{Serialize, Deserialize}; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Image { + pub name: String, + /// colour indexes - todo convert to [u8; 64]? + pub pixels: Vec, +} + +impl Image { + fn from(intermediate: IntermediateImage) -> Image { + Image { + name: intermediate.name.to_owned(), + pixels: intermediate.pixels.split_whitespace().collect::().chars().map( + |char|char as u8 + ).collect() + } + } +} + +/// for toml purposes +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct IntermediateImages { + /// singular so each image is named "image" instead of "images" in toml + image: Vec, +} + +impl IntermediateImages { + fn to_images(&self) -> Vec { + self.image.iter().map(|intermediate| + Image::from(intermediate.clone()) + ).collect() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct IntermediateImage { + name: String, + pixels: String, +} + +impl IntermediateImage { + // todo refactor + fn from(image: Image) -> IntermediateImage { + let mut string = "\n".to_string(); + + let sqrt = (image.pixels.len() as f64).sqrt() as usize; + for line in image.pixels.chunks(sqrt) { + for pixel in line { + string.push_str(&format!("{}", *pixel)); + } + string.push('\n'); + } + + IntermediateImage { + name: image.name.to_owned(), + /// todo wtf? I guess this crate doesn't handle multiline strings correctly + pixels: format!("\"\"{}\"\"", string), + } + } +} + +#[cfg(test)] +mod test { + // #[test] + // fn test_image_from_toml() { + // let str = include_str!("test-resources/basic/images.toml"); + // let output: Image = toml::from_str(str).unwrap(); + // let expected = crate::mock::image::avatar(); + // assert_eq!(output, expected); + // } +} diff --git a/src/lib.rs b/src/lib.rs index b518327..db19978 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,83 +1,18 @@ -use std::collections::HashMap; use std::fs; +use std::path::PathBuf; use serde_derive::{Serialize, Deserialize}; -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Colour { - red: u8, - green: u8, - blue: u8, - alpha: u8, -} +mod colour; +mod config; +mod image; +mod mock; +mod palette; +mod room; -impl Colour { - pub fn from(colours: Vec) -> Colour { - const ZERO: u8 = 0; - Colour { - red: *colours.get(0).unwrap_or(&ZERO), - green: *colours.get(1).unwrap_or(&ZERO), - blue: *colours.get(2).unwrap_or(&ZERO), - alpha: *colours.get(3).unwrap_or(&255), - } - } - - pub fn to_vec(&self) -> Vec { - vec![self.red, self.green, self.blue, self.alpha] - } -} - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Palette { - name: String, - colours: Vec, -} - -impl Palette { - pub fn from(name: &str, toml: &str) -> Self { - let intermediate: IntermediatePalette = toml::from_str(toml).unwrap(); - - println!("palette name: {}", name); - - for colour in &intermediate.colours { - println!("palette colour: {}{}{}", colour[0], colour[1], colour[2]); - } - - Palette { - name: name.to_string(), - colours: intermediate.colours.iter().map(|vec| { - Colour::from(vec.clone()) - }).collect(), - } - } - - pub fn from_file(path: String) -> Self { - // todo get name without extension - let name = "blah"; - let toml = fs::read_to_string(path).unwrap(); - Self::from(name, &toml) - } -} - -/// for toml purposes -#[derive(Serialize, Deserialize)] -struct IntermediatePalette { - colours: Vec>, -} - -/// for toml purposes -#[derive(Serialize, Deserialize)] -struct IntermediatePalettes { - /// singular so each palette section is named "palette" instead of "palettes" in toml - palette: Vec, -} - -impl IntermediatePalettes { - pub fn from_dir() -> Self { - Self { - palette: vec![] - } - } -} +pub use colour::Colour; +pub use palette::Palette; +pub use palette::IntermediatePalette; +use crate::config::Config; #[derive(Eq, Hash, PartialEq)] pub struct Position { @@ -85,66 +20,6 @@ pub struct Position { y: u8, } -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Image { - name: String, - /// colour indexes - pixels: Vec, -} - -impl Image { - fn from(intermediate: IntermediateImage) -> Image { - Image { - name: intermediate.name.to_owned(), - pixels: intermediate.pixels.split_whitespace().collect::().chars().map( - |char|char as u8 - ).collect() - } - } -} - -/// for toml purposes -#[derive(Debug, Serialize, Deserialize)] -struct IntermediateImages { - /// singular so each image is named "image" instead of "images" in toml - image: Vec, -} - -impl IntermediateImages { - fn to_images(&self) -> Vec { - self.image.iter().map(|intermediate| - Image::from(intermediate.clone()) - ).collect() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct IntermediateImage { - name: String, - pixels: String, -} - -impl IntermediateImage { - // todo refactor - fn from(image: Image) -> IntermediateImage { - let mut string = "\n".to_string(); - - let sqrt = (image.pixels.len() as f64).sqrt() as usize; - for line in image.pixels.chunks(sqrt) { - for pixel in line { - string.push_str(&format!("{}", *pixel)); - } - string.push('\n'); - } - - IntermediateImage { - name: image.name.to_owned(), - /// todo wtf? I guess this crate doesn't handle multiline strings correctly - pixels: format!("\"\"{}\"\"", string), - } - } -} - // #[derive(Serialize, Deserialize)] // pub struct Thing { // name: Option, @@ -153,42 +28,6 @@ impl IntermediateImage { // } // -pub struct Room { - name: String, - width: u8, - height: u8, - /// thing names and their positions - background: HashMap, - foreground: HashMap, -} - -#[derive(Serialize, Deserialize)] -struct IntermediateRoom { - name: String, - background: Vec, - foreground: Vec, -} - -impl IntermediateRoom { - fn from(room: Room) -> IntermediateRoom { - fn hashmap_to_vec(hash: HashMap, width: u8, height: u8) -> Vec { - let mut thing_ids = Vec::new(); - - while thing_ids.len() < (width * height) as usize { - thing_ids.push(String::new()); - } - - thing_ids - } - - IntermediateRoom { - name: "".to_string(), - background: vec![], - foreground: vec![] - } - } -} - // #[derive(Serialize, Deserialize)] // pub enum DataType { // Image, @@ -261,18 +100,6 @@ impl IntermediateRoom { // } // } -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Config { - /// used in the window title bar - name: Option, - width: u8, - height: u8, - /// animation rate in milliseconds - tick: u64, - /// if this is not specified, the game will pick the first room it finds - starting_room: Option, -} - #[derive(Serialize, Deserialize)] pub struct Game { config: Config, @@ -280,225 +107,36 @@ pub struct Game { // variables: Vec, // triggers: HashMap, } -// -// #[derive(Debug)] -// pub struct GameParseError; -// -// impl Game { -// pub fn from(s: &str) -> Result { -// let result = toml::from_str(s); -// if result.is_ok() { -// Ok(result.unwrap()) -// } else { -// Err(GameParseError) -// } -// } -// } -// -mod mock { - pub(crate) mod image { - use crate::Image; +#[derive(Debug)] +pub struct GameParseError; - pub fn bg() -> Image { - Image { - name: "bg".to_string(), - pixels: vec![ - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - ] - } +impl Game { + pub fn from(path: String) -> Result { + let path = PathBuf::from(path); + + let mut palettes_dir = path.clone(); + palettes_dir.push("palettes"); + let palette_files = palettes_dir.read_dir() + .expect("couldn't find any palettes"); + + for file in palette_files { + let file = file.unwrap(); + println!("palette found: {:?}", file.file_name()); } - pub fn block() -> Image { - Image { - name: "block".to_string(), - pixels: vec![ - 1,1,1,1,1,1,1,1, - 1,0,0,0,0,0,0,1, - 1,0,0,0,0,0,0,1, - 1,0,0,1,1,0,0,1, - 1,0,0,1,1,0,0,1, - 1,0,0,0,0,0,0,1, - 1,0,0,0,0,0,0,1, - 1,1,1,1,1,1,1,1, - ] - } - } + // todo load config + let mut game_config = path.clone(); + game_config.push("game.toml"); + let config = fs::read_to_string(game_config) + .expect("Couldn't load game config"); + let config: Config = toml::from_str(&config) + .expect("Couldn't parse game config"); - pub fn avatar() -> Image { - Image { - name: "avatar".to_string(), - pixels: vec![ - 0,0,0,2,2,0,0,0, - 0,0,0,2,2,0,0,0, - 0,0,0,2,2,0,0,0, - 0,0,2,2,2,2,0,0, - 0,2,2,2,2,2,2,0, - 2,0,2,2,2,2,0,2, - 0,0,2,0,0,2,0,0, - 0,0,2,0,0,2,0,0, - ] + Ok( + Game { + config, } - } - - pub fn cat() -> Image { - Image { - name: "cat".to_string(), - pixels: vec![ - 0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0, - 0,2,0,2,0,0,0,2, - 0,2,2,2,0,0,0,2, - 0,2,2,2,0,0,2,0, - 0,2,2,2,2,2,0,0, - 0,0,2,2,2,2,0,0, - 0,0,2,0,0,2,0,0, - ] - } - } - } - - pub(crate) mod palette { - use crate::{Palette, Colour, IntermediatePalette}; - - pub(crate) fn intermediate() -> IntermediatePalette { - IntermediatePalette { - colours: vec![ - vec![0,0,0,0], - vec![0,81,104,255], - vec![118,159,155,255], - vec![155,155,155,255], - ] - } - } - - pub(crate) fn default() -> Palette { - Palette { - name: "blueprint".to_string(), - colours: vec![ - Colour { red: 0, green: 0, blue: 0, alpha: 0 }, - Colour { red: 0, green: 81, blue: 104, alpha: 255 }, - Colour { red: 118, green: 159, blue: 155, alpha: 255 }, - Colour { red: 155, green: 155, blue: 155, alpha: 255 }, - ], - } - } + ) } } - -#[cfg(test)] -mod test { - use crate::{Game, Config, Palette, Colour, Image, IntermediatePalettes, IntermediateImage, IntermediateImages}; - - #[test] - fn test_config_from_toml() { - let output: Config = toml::from_str(include_str!("test-resources/basic/game.toml")).unwrap(); - let expected = Config { - name: Some("Write your game's title here".to_string()), - width: 16, - height: 9, - tick: 400, - starting_room: Some("example room".to_string()) - }; - assert_eq!(output, expected); - } - - #[test] - fn test_config_to_toml() { - let output = toml::to_string(&Config { - name: Some("Write your game's title here".to_string()), - width: 16, - height: 9, - tick: 400, - starting_room: Some("example room".to_string()) - }).unwrap(); - let expected = include_str!("test-resources/basic/game.toml"); - assert_eq!(&output, expected); - } - - #[test] - fn test_palette_from_toml() { - let output = Palette::from( - "blueprint", - include_str!("test-resources/basic/palettes/blueprint.toml") - ); - let expected = crate::mock::palette::default(); - assert_eq!(output, expected); - } - - #[test] - fn test_palette_to_toml() { - let intermediate = crate::mock::palette::intermediate(); - let output = toml::to_string(&intermediate).unwrap(); - let expected = include_str!("test-resources/basic/palettes/blueprint.toml"); - assert_eq!(&output, expected); - } - - // #[test] - // fn test_image_from_toml() { - // let str = include_str!("test-resources/basic/images.toml"); - // let output: Image = toml::from_str(str).unwrap(); - // let expected = crate::mock::image::avatar(); - // assert_eq!(output, expected); - // } - - #[test] - fn test_colour_from_intermediate() { - let output = Colour::from(vec![64, 128, 192, 255]); - let expected = Colour { red: 64, green: 128, blue: 192, alpha: 255 }; - assert_eq!(output, expected); - } - - #[test] - fn test_colour_to_intermediate() { - let output = Colour { red: 64, green: 128, blue: 192, alpha: 255 }.to_vec(); - let expected = vec![64, 128, 192, 255]; - assert_eq!(output, expected); - } - - // #[test] - // fn test_images_from_intermediate() { - // let str = include_str!("test-resources/basic/images.toml"); - // let output: Vec = toml::from_str(str).unwrap(); - // print!("{}", output.len()); - // } - - // #[test] - // fn test_images_to_toml() { - // let images = IntermediateImages { - // image: vec![ - // IntermediateImage::from(crate::mock::image::bg()), - // IntermediateImage::from(crate::mock::image::block()), - // IntermediateImage::from(crate::mock::image::avatar()), - // IntermediateImage::from(crate::mock::image::cat()), - // ] - // }; - // - // let output = toml::to_string(&images).unwrap(); - // let expected = include_str!("test-resources/basic/images.toml"); - // - // // I think this is failing because one has escaped quotation marks and one has normal ones(??) - // assert_eq!(output, expected); - // } - - // #[test] - // fn test_images_from_toml() { - // let str = include_str!("test-resources/basic/images.toml"); - // let output: IntermediateImages = toml::from_str(str).unwrap(); - // let output = output.to_images(); - // let expected = vec![ - // crate::mock::image::bg(), - // crate::mock::image::block(), - // crate::mock::image::avatar(), - // crate::mock::image::cat(), - // ]; - // assert_eq!(output, expected); - // } -} diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..d5ad745 --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,94 @@ +pub(crate) mod image { + use crate::image::Image; + + pub fn bg() -> Image { + Image { + name: "bg".to_string(), + pixels: vec![ + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + ] + } + } + + pub fn block() -> Image { + Image { + name: "block".to_string(), + pixels: vec![ + 1,1,1,1,1,1,1,1, + 1,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,1, + 1,0,0,1,1,0,0,1, + 1,0,0,1,1,0,0,1, + 1,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,1, + 1,1,1,1,1,1,1,1, + ] + } + } + + pub fn avatar() -> Image { + Image { + name: "avatar".to_string(), + pixels: vec![ + 0,0,0,2,2,0,0,0, + 0,0,0,2,2,0,0,0, + 0,0,0,2,2,0,0,0, + 0,0,2,2,2,2,0,0, + 0,2,2,2,2,2,2,0, + 2,0,2,2,2,2,0,2, + 0,0,2,0,0,2,0,0, + 0,0,2,0,0,2,0,0, + ] + } + } + + pub fn cat() -> Image { + Image { + name: "cat".to_string(), + pixels: vec![ + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,2,0,2,0,0,0,2, + 0,2,2,2,0,0,0,2, + 0,2,2,2,0,0,2,0, + 0,2,2,2,2,2,0,0, + 0,0,2,2,2,2,0,0, + 0,0,2,0,0,2,0,0, + ] + } + } +} + +pub(crate) mod palette { + use crate::{Palette, Colour, IntermediatePalette}; + + pub(crate) fn intermediate() -> IntermediatePalette { + IntermediatePalette { + colours: vec![ + vec![0,0,0,0], + vec![0,81,104,255], + vec![118,159,155,255], + vec![155,155,155,255], + ] + } + } + + pub(crate) fn default() -> Palette { + Palette { + name: "blueprint".to_string(), + colours: vec![ + Colour { red: 0, green: 0, blue: 0, alpha: 0 }, + Colour { red: 0, green: 81, blue: 104, alpha: 255 }, + Colour { red: 118, green: 159, blue: 155, alpha: 255 }, + Colour { red: 155, green: 155, blue: 155, alpha: 255 }, + ], + } + } +} diff --git a/src/palette.rs b/src/palette.rs new file mode 100644 index 0000000..9793d3d --- /dev/null +++ b/src/palette.rs @@ -0,0 +1,80 @@ +use serde_derive::{Serialize, Deserialize}; + +use crate::colour::Colour; +use std::fs; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Palette { + pub name: String, + pub colours: Vec, +} + +impl Palette { + pub fn from(name: &str, toml: &str) -> Self { + let intermediate: IntermediatePalette = toml::from_str(toml).unwrap(); + + println!("palette name: {}", name); + + for colour in &intermediate.colours { + println!("palette colour: {}{}{}", colour[0], colour[1], colour[2]); + } + + Palette { + name: name.to_string(), + colours: intermediate.colours.iter().map(|vec| { + Colour::from(vec.clone()) + }).collect(), + } + } + + pub fn from_file(path: String) -> Self { + // todo get name without extension + let name = "blah"; + let toml = fs::read_to_string(path).unwrap(); + Self::from(name, &toml) + } +} + +/// for toml purposes +#[derive(Serialize, Deserialize)] +pub struct IntermediatePalette { + pub colours: Vec>, +} + +/// for toml purposes +#[derive(Serialize, Deserialize)] +struct IntermediatePalettes { + /// singular so each palette section is named "palette" instead of "palettes" in toml + palette: Vec, +} + +impl IntermediatePalettes { + pub fn from_dir() -> Self { + Self { + palette: vec![] + } + } +} + +#[cfg(test)] +mod test { + use crate::Palette; + + #[test] + fn test_palette_from_toml() { + let output = Palette::from( + "blueprint", + include_str!("test-resources/basic/palettes/blueprint.toml") + ); + let expected = crate::mock::palette::default(); + assert_eq!(output, expected); + } + + #[test] + fn test_palette_to_toml() { + let intermediate = crate::mock::palette::intermediate(); + let output = toml::to_string(&intermediate).unwrap(); + let expected = include_str!("test-resources/basic/palettes/blueprint.toml"); + assert_eq!(&output, expected); + } +} diff --git a/src/room.rs b/src/room.rs new file mode 100644 index 0000000..27a8ee2 --- /dev/null +++ b/src/room.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use serde_derive::{Serialize, Deserialize}; + +use crate::Position; + +pub struct Room { + pub name: String, + pub width: u8, + pub height: u8, + /// thing names and their positions + pub background: HashMap, + pub foreground: HashMap, +} + +/// todo &str? +#[derive(Serialize, Deserialize)] +struct IntermediateRoom { + name: String, + background: Vec, + foreground: Vec, +} + +impl IntermediateRoom { + fn from(room: Room) -> IntermediateRoom { + fn hashmap_to_vec(hash: HashMap, width: u8, height: u8) -> Vec { + let mut thing_ids = Vec::new(); + + while thing_ids.len() < (width * height) as usize { + thing_ids.push(String::new()); + } + + thing_ids + } + + IntermediateRoom { + name: "".to_string(), + background: vec![], + foreground: vec![] + } + } +}