From f4b01fce86e5dcaeeb3085cf3f10ec10a5061810 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 27 Feb 2023 20:54:36 +0100 Subject: [PATCH] Added Map select screen --- Cargo.lock | 35 ++- Cargo.toml | 3 +- README.md | 30 +++ logo.png => assets/icon.png | Bin src/draw.rs | 441 +++++++++++++++++++++++++++++++++--- src/main.rs | 2 + src/screens/build.rs | 71 +++--- src/screens/inventory.rs | 56 ++++- src/screens/map_select.rs | 211 +++++++++++++++++ src/screens/mod.rs | 7 +- src/screens/welcome.rs | 112 +++++---- src/storage.rs | 94 ++++++++ src/textures.rs | 13 +- 13 files changed, 944 insertions(+), 131 deletions(-) rename logo.png => assets/icon.png (100%) create mode 100644 src/screens/map_select.rs create mode 100644 src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index bde5830..4858d68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,6 +713,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.3" @@ -1598,9 +1619,10 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "little_town" -version = "0.0.1" +version = "0.1.0" dependencies = [ "async-std", + "directories-next", "futures", "libp2p", "macroquad", @@ -2526,6 +2548,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.8", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.7.1" diff --git a/Cargo.toml b/Cargo.toml index df7f398..4038144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "little_town" description = "Build a small isometric town" -version = "0.0.1" +version = "0.1.0" edition = "2021" authors = [ "Jakob Meier " ] readme = "README.org" @@ -18,6 +18,7 @@ multiplayer = ["libp2p", "futures", "async-std", "quad-rand"] macroquad = "0.3.25" nanoserde = "0.1.32" quad-storage = "0.1.3" +directories-next = "2.0.0" # p2p dependencies libp2p = { version = "0.51.0", features = [ diff --git a/README.md b/README.md index fd7b40b..99d96a4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,36 @@ because the file will also be moved. - [ ] Settings menu (i.e for Keybindings) - [ ] Game music? +## Keymap +### in-Game +| Action | Key(s) / MouseButton | +|--------|----------------------| +| place Block | Left mouse button | +| destroy Block | right mouse button | +| Save Game | `S` | +| Save & Quit | `Q` or `` | + +#### Block orientations +| Action | Key(s) / MouseButton | +|--------|----------------------| +| North | `K` | +| South | `J` | +| West | `H` | +| East | `L` || + +#### Camera movement +| Action | Key(s) / MouseButton | +|--------|----------------------| +| Up/North | `` | +| Down/South | `` | +| Left | `` | +| Right | `` | + + +### Inventory + +### Level Select screen + ## Building We are currently building binaries for some distros using the Sourcehut CI. diff --git a/logo.png b/assets/icon.png similarity index 100% rename from logo.png rename to assets/icon.png diff --git a/src/draw.rs b/src/draw.rs index d4392b4..1260ec6 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,41 +1,408 @@ use macroquad::prelude::*; use crate::textures::AssetStore; -/// draws centered text -/// with the given font and font size -pub fn draw_centered_text(text: &str, x: f32, y: f32, font: Option, font_size: u16, color: Color) { - let mut title_params = TextParams { - font_size, - color, - ..Default::default() - }; - if let Some(font) = font { - title_params.font = font; - } - let title_center = get_text_center( - text, - font, - font_size, - 1.0, - 0.0); - draw_text_ex( - text, - x - title_center.x, - y - title_center.y, - title_params); -} - -/// draws a wide button with text -pub fn draw_wide_button(text: &str, pressed: bool, x: f32, y: f32, assets: &AssetStore) { - draw_texture( - if pressed { - assets.ui.long_button.1 - } else { assets.ui.long_button.0 }, - x - 190.0 / 2.0, - y, - WHITE); - - draw_centered_text(text, - x, y + 49.0 / 2.0 - 2.0, - Some(assets.font), 60, LIGHTGRAY); +/// A UI item that can be rendered +pub trait Widget { + /// Message types returned from the event loop + type Event; + /// Callback used to redraw the component + async fn draw(&self, assets: &AssetStore); + /// Callback used to perform background tasks + /// e.g. keyboard input + /// NOTE: this is currently not being run asynchronously + /// when running large operations, spawn a separate thread + fn ev_loop(&mut self) -> Self::Event; + // returns the width and height of the widget + fn get_dimensions(&self) -> (f32, f32); +} + +/// UI Widget +/// used to draw centered text +pub struct CenteredText { + /// middle of widget (along the x axis) + /// set in percent of the display width + x: f32, + /// middle of widget (along the y axis) + /// set in percent of the display height + y: f32, + /// the text to draw + text: String, + /// custom font size to use + /// default is 60 + font_size: Option, + /// the font to use + /// defaults to macroquad default font + font: Option, + /// text color + /// defaults to white + color: Option +} +impl CenteredText { + /// create new centered-text widget + pub fn new(text: &str, x: f32, y: f32) -> Self { + Self { + x, + y, + text: text.to_string(), + font_size: None, + font: None, + color: None + } + } + /// changes the text of the widget + pub fn set_text(self, text: &str) -> Self { + Self { + text: text.to_string(), + ..self + } + } + /// sets the font size + pub fn with_font_size(self, font_size: u16) -> Self { + Self { + font_size: Some(font_size), + ..self + } + } + /// sets the font + pub fn with_font(self, font: Font) -> Self { + Self { + font: Some(font), + ..self + } + } + /// sets a custom color + pub fn with_color(self, color: Color) -> Self { + Self { + color: Some(color), + ..self + } + } +} +impl Widget for CenteredText { + type Event = bool; + async fn draw(&self, assets: &AssetStore) { + let (x,y) = (screen_width() * self.x, screen_height() * self.y); + + let mut title_params = TextParams { + font_size: self.font_size.unwrap_or(60), + color: self.color.unwrap_or(WHITE), + ..Default::default() + }; + if let Some(font) = self.font { + title_params.font = font; + } + let title_center = get_text_center( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0, + 0.0); + draw_text_ex( + &self.text, + x - title_center.x, + y- title_center.y, + title_params); + } + fn ev_loop(&mut self) -> Self::Event { + true + } + fn get_dimensions(&self) -> (f32, f32) { + let text_dim: TextDimensions = measure_text( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0); + (text_dim.width, text_dim.height) + } +} + +/// A button event send from the Button widget ev-loop +/// i.e WideButton +pub enum ButtonEvent { + /// nothing happened + None, + /// user clicked on button with left mouse button + LeftClick, + /// user clicked on button with right mouse button + RightClick +} +/// UI Widget +/// a clickable button with text on it +pub struct TextButton { + /// the text on the button + text: String, + /// middle of button (x axis) + /// specified in perfect of screen width + x: f32, + /// middle of button (y-axis) + /// specified in perfect of screen height + y: f32, + /// the font used to draw the text + font: Option, + /// custom font color + font_color: Option, + /// custom font size to use + font_size: Option, + /// true if mouse is over button + hovered: bool +} +impl TextButton { + /// creates a new text button widget + pub fn new(text: &str, x: f32, y: f32) -> Self { + Self { + text: text.to_string(), + x, + y, + font: None, + font_color: None, + font_size: None, + hovered: false + } + } + /// sets the custom font + pub fn with_font(self, font: Font) -> Self { + Self { + font: Some(font), + ..self + } + } + /// sets the font color + pub fn with_font_color(self, color: Color) -> Self { + Self { + font_color: Some(color), + ..self + } + } + /// sets the font size + pub fn with_font_size(self, size: u16) -> Self { + Self { + font_size: Some(size), + ..self + } + } +} +impl Widget for TextButton { + type Event = ButtonEvent; + fn get_dimensions(&self) -> (f32, f32) { + let text_dim: TextDimensions = measure_text( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0); + (text_dim.width + 30.0, text_dim.height+20.0) + } + async fn draw(&self, assets: &AssetStore) { + let (x,y) = (screen_width() * self.x, screen_height() * self.y); + let (width, height) = self.get_dimensions(); + + draw_texture_ex( + if self.hovered { assets.ui.long_button.1 } else { assets.ui.long_button.0 }, + x - width / 2.0, + y - height / 2.0, + WHITE, + DrawTextureParams { + dest_size: Some(Vec2::new(width, height)), + source: None, + rotation: 0.0, + flip_x: false, + flip_y: false, + pivot: None + }); + + let text_center = get_text_center( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0, + 0.0); + draw_text_ex( + &self.text, + x - text_center.x, + y - text_center.y, + TextParams { + font_size: self.font_size.unwrap_or(60), + color: self.font_color.unwrap_or(WHITE), + ..Default::default() + }); + } + fn ev_loop(&mut self) -> Self::Event { + self.hovered = false; + + let (width, height) = self.get_dimensions(); + + let (mx, my) = mouse_position(); + let (x,y) = (screen_width() * self.x, screen_height() * self.y); + + if mx >= x - width/2.0 && mx <= x + width/2.0 + && my >= y - height/2.0 && my <= y + height/2.0 { + self.hovered = true; + + if is_mouse_button_pressed(MouseButton::Left) { + return Self::Event::LeftClick; + } + if is_mouse_button_pressed(MouseButton::Right) { + return Self::Event::RightClick; + } + } + + + Self::Event::None + } +} + + +/// A widget similar to a button, +/// but it can be selected +/// and will stay selected +pub struct SelectableText { + /// the text on the button + text: String, + /// middle of button (x axis) + /// specified in perfect of screen width + x: f32, + /// middle of button (y-axis) + /// specified in perfect of screen height + y: f32, + /// the font used to draw the text + font: Option, + /// custom font color + /// defaults to white + font_color: Option, + /// custom font size to use + font_size: Option, + /// true if widget option is selected + selected: bool, + /// if the widget is visible or not + visible: bool +} +impl SelectableText { + /// creates a new text button widget + pub fn new(text: &str, x: f32, y: f32) -> Self { + Self { + text: text.to_string(), + x, + y, + font: None, + font_color: None, + font_size: None, + selected: false, + visible: true + } + } + /// set the visibility + pub fn with_visibility(self, vis: bool) -> Self { + Self { + visible: vis, + ..self + } + } + /// sets the custom font + pub fn with_font(self, font: Font) -> Self { + Self { + font: Some(font), + ..self + } + } + /// sets the font color + pub fn with_font_color(self, color: Color) -> Self { + Self { + font_color: Some(color), + ..self + } + } + /// sets the font size + pub fn with_font_size(self, size: u16) -> Self { + Self { + font_size: Some(size), + ..self + } + } + /// selects or unselects the Widget + pub fn set_selected(&mut self, state: bool) { + self.selected = state; + } + /// updates the widget text + pub fn set_text(&mut self, text: &str) { + self.text = text.to_string(); + } + /// updates the widget visibility + pub fn set_visibility(&mut self, vis: bool) { + self.visible = vis; + } +} +impl Widget for SelectableText { + type Event = ButtonEvent; + fn get_dimensions(&self) -> (f32, f32) { + let text_dim: TextDimensions = measure_text( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0); + (text_dim.width + 30.0, text_dim.height+20.0) + } + async fn draw(&self, assets: &AssetStore) { + if !self.visible { + return; + } + + let (x,y) = (screen_width() * self.x, screen_height() * self.y); + let (width, height) = self.get_dimensions(); + + let mut bg = assets.ui.panel_brown.0; + if self.selected { + // draw blue background + bg = assets.ui.panel_blue.0; + } + // draw background + draw_texture_ex( + bg, + x - width/2.0, + y - height/2.0, + WHITE, + DrawTextureParams { + dest_size: Some(Vec2::new(width, height)), + ..Default::default() + } + ); + + let text_center = get_text_center( + &self.text, + self.font, + self.font_size.unwrap_or(60), + 1.0, + 0.0); + draw_text_ex( + &self.text, + x - text_center.x, + y - text_center.y, + TextParams { + font_size: self.font_size.unwrap_or(60), + color: self.font_color.unwrap_or(WHITE), + ..Default::default() + }); + } + fn ev_loop(&mut self) -> Self::Event { + if !self.visible { + return Self::Event::None; + } + + let (width, height) = self.get_dimensions(); + + let (mx, my) = mouse_position(); + let (x,y) = (screen_width() * self.x, screen_height() * self.y); + + if mx >= x - width/2.0 && mx <= x + width/2.0 + && my >= y - height/2.0 && my <= y + height/2.0 { + if is_mouse_button_pressed(MouseButton::Left) { + return Self::Event::LeftClick; + } + if is_mouse_button_pressed(MouseButton::Right) { + return Self::Event::RightClick; + } + } + + + Self::Event::None + + } } diff --git a/src/main.rs b/src/main.rs index e591cea..54fe92e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![feature(int_roundings)] #![feature(let_chains)] #![feature(async_fn_in_trait)] +#![feature(associated_type_defaults)] use macroquad::prelude::*; @@ -10,6 +11,7 @@ mod textures; mod types; mod blocks; mod draw; +mod storage; #[cfg(feature = "multiplayer")] mod p2p; diff --git a/src/screens/build.rs b/src/screens/build.rs index dcbed25..1b0b362 100644 --- a/src/screens/build.rs +++ b/src/screens/build.rs @@ -8,7 +8,12 @@ use crate::types::{ Direction }; use crate::blocks::Block; -use super::inventory::Inventory; +use super::{ + inventory::Inventory, + Screen, + welcome::WelcomeScreen +}; +use crate::storage::save_map; use nanoserde::{DeJson, SerJson}; #[cfg(feature = "multiplayer")] @@ -73,25 +78,19 @@ pub struct BuildScreen { mouse_position: Option<(f32, f32)>, #[cfg(feature="multiplayer")] #[nserde(skip)] - multiplayer: Option + multiplayer: Option, + #[nserde(skip)] + file_name: Option } impl BuildScreen { - pub fn new() -> Self { - - // load from disk if possible - let storage = &mut quad_storage::STORAGE.lock().unwrap(); - if let Some(dt) = storage.get("build-map") { - if let Ok(bscr) = BuildScreen::deserialize_json(&dt) { - return bscr; - } - } - + pub fn new(file_name: String) -> Self { let mut this = Self { grid: HashMap::new(), cam: Camera::new(), show_inv: false, inv: Inventory::new(), mouse_position: None, + file_name: Some(file_name), #[cfg(feature="multiplayer")] multiplayer: None }; @@ -103,21 +102,30 @@ impl BuildScreen { /// creates a new BuildScreen instance /// and starts the p2p multiplayer listener #[cfg(feature="multiplayer")] - pub fn multiplayer(is_host: bool) -> Self { - if is_host { - let mut this = Self::new(); - this.multiplayer = Some(Peer::host()); - return this; - } else { + pub fn multiplayer() -> Self { Self { grid: HashMap::new(), cam: Camera::new(), show_inv: false, inv: Inventory::empty(), mouse_position: None, - #[cfg(feature="multiplayer")] + file_name: None, multiplayer: Some(Peer::client()) - } + } + } + + pub fn with_file_name(self, file_name: &str) -> Self { + Self { + file_name: Some(file_name.to_string()), + ..self + } + } + + #[cfg(feature="multiplayer")] + pub fn as_host(self) -> Self { + Self { + multiplayer: Some(Peer::host()), + ..self } } } @@ -472,34 +480,37 @@ impl GameComponent for BuildScreen { } // change block direction - if is_key_pressed(KeyCode::W) { + if is_key_pressed(KeyCode::K) { self.inv.direction = Direction::North; } - if is_key_pressed(KeyCode::D) { + if is_key_pressed(KeyCode::L) { self.inv.direction = Direction::East; } - if is_key_pressed(KeyCode::S) { + if is_key_pressed(KeyCode::J) { self.inv.direction = Direction::South; } - if is_key_pressed(KeyCode::A) { + if is_key_pressed(KeyCode::H) { self.inv.direction = Direction::West; } // save the world - if is_key_pressed(KeyCode::N) || is_key_pressed(KeyCode::Q) { + if is_key_pressed(KeyCode::S) + || is_key_pressed(KeyCode::Q) + || is_key_pressed(KeyCode::Escape) { // save game - let storage = &mut quad_storage::STORAGE.lock().unwrap(); - let json = BuildScreen::serialize_json(&self); - storage.set("build-map", &json); - if is_key_pressed(KeyCode::Q) { + if let Some(file_name) = &self.file_name { + save_map(&file_name, &self); + } + + if is_key_pressed(KeyCode::Q) || is_key_pressed(KeyCode::Escape) { if let Some(mp) = &mut self.multiplayer { mp.close(); } // close game - return GameEvent::Quit; + return GameEvent::ChangeScreen(Screen::Welcome(WelcomeScreen::new())); } } diff --git a/src/screens/inventory.rs b/src/screens/inventory.rs index 1ce859e..5fb0e8f 100644 --- a/src/screens/inventory.rs +++ b/src/screens/inventory.rs @@ -11,9 +11,53 @@ use super::build::{ }; use crate::textures::AssetStore; use crate::blocks::Block; -use crate::draw::draw_centered_text; use std::collections::HashMap; use nanoserde::{DeJson, SerJson}; +use crate::draw::{ + TextButton, + CenteredText, + Widget, + ButtonEvent +}; + +/// draws centered text +/// with the given font and font size +pub fn draw_centered_text(text: &str, x: f32, y: f32, font: Option, font_size: u16, color: Color) { + let mut title_params = TextParams { + font_size, + color, + ..Default::default() + }; + if let Some(font) = font { + title_params.font = font; + } + let title_center = get_text_center( + text, + font, + font_size, + 1.0, + 0.0); + draw_text_ex( + text, + x - title_center.x, + y - title_center.y, + title_params); +} + +/// draws a wide button with text +pub fn draw_wide_button(text: &str, pressed: bool, x: f32, y: f32, assets: &AssetStore) { + draw_texture( + if pressed { + assets.ui.long_button.1 + } else { assets.ui.long_button.0 }, + x - 190.0 / 2.0, + y, + WHITE); + + draw_centered_text(text, + x, y + 49.0 / 2.0 - 2.0, + Some(assets.font), 60, LIGHTGRAY); +} /// The players inventory #[derive(SerJson, DeJson)] @@ -173,16 +217,6 @@ impl GameComponent for Inventory { } ); - // draw name - draw_centered_text( - block.get_name(), - tx + slot_render_dim / 2.0, - ty + 4.0 * 7.0, - Some(assets.font), - 40, - WHITE - ); - // draw amount let text = if self.infinite_items { "inf.".to_string() } else { amount.to_string() }; draw_centered_text( diff --git a/src/screens/map_select.rs b/src/screens/map_select.rs new file mode 100644 index 0000000..f20aea5 --- /dev/null +++ b/src/screens/map_select.rs @@ -0,0 +1,211 @@ +use macroquad::prelude::*; +use crate::types::{ + GameComponent, + GameEvent +}; +use crate::textures::AssetStore; +use crate::draw::{ + TextButton, + Widget, + ButtonEvent, + SelectableText +}; +use crate::storage::{ + get_world_list, + read_map +}; +use super::{ + Screen, + welcome::WelcomeScreen +}; + +/// The map select screen +/// also allows creation of new maps +pub struct SelectScreen { + /// list of all world/file names + list: Vec, + /// back button widget + /// (goes back to WelcomeScreen) + widget_back: TextButton, + /// host button widget + /// launch world in multiplayer(host mode) + #[cfg(feature = "multiplayer")] + widget_host: TextButton, + /// play button widget + /// launch world in singleplayer mode + widget_play: TextButton, + /// new world button widget + /// launch world creation screen + widget_new: TextButton, + /// list of slots + level_select_widgets: Vec, + /// currently selected slot + selected: Option, + /// currently visible page + page: usize +} +impl SelectScreen { + /// create a new MapSelectorScreen instance + pub fn new() -> Self { + let levels = get_world_list(); + Self { + list: levels.clone(), + widget_back: TextButton::new("Back", 0.1, 0.1) + .with_font_size(50) + .with_font_color(LIGHTGRAY), + #[cfg(feature = "multiplayer")] + widget_host: TextButton::new("Host", 0.35, 0.8) + .with_font_size(30), + widget_play: TextButton::new("Play", 0.65, 0.8) + .with_font_size(30), + widget_new: TextButton::new("New", 0.5, 0.8) + .with_font_size(30), + level_select_widgets: vec![ + SelectableText::new(levels.get(0).unwrap_or(&"???".to_string()), 0.5, 0.4) + .with_visibility(levels.len() >= 1), + SelectableText::new(levels.get(1).unwrap_or(&"???".to_string()), 0.5, 0.5) + .with_visibility(levels.len() >= 2), + SelectableText::new(levels.get(2).unwrap_or(&"???".to_string()), 0.5, 0.6) + .with_visibility(levels.len() >= 3), + ], + selected: None, + page: 0 + } + } +} +impl GameComponent for SelectScreen { + async fn draw(&self, assets: &AssetStore) { + { + // draw background image + let dim: f32 = if screen_width() < screen_height() { + screen_width() * 2.0/3.0 + } else { screen_height() * 2.0/3.0 }; + draw_texture_ex( + assets.icon, + screen_width()/2.0 - dim/2.0, + screen_height()/2.0 - dim/2.0, + WHITE, + DrawTextureParams { + dest_size: Some(Vec2::new(dim, dim)), + ..Default::default() + } + ); + draw_rectangle(0.0, 0.0, screen_width(), screen_height(), Color::from_rgba(0, 0, 0, 100)); + } + + self.widget_back.draw(&assets).await; + + for slot in self.level_select_widgets.iter() { + slot.draw(&assets).await; + } + + #[cfg(feature = "multiplayer")] + self.widget_host.draw(&assets).await; + self.widget_new.draw(&assets).await; + self.widget_play.draw(&assets).await; + } + fn ev_loop(&mut self) -> GameEvent { + match self.widget_back.ev_loop() { + ButtonEvent::LeftClick => { + // return to welcome screen + return GameEvent::ChangeScreen(Screen::Welcome(WelcomeScreen::new())) + }, + _ => () + } + + #[cfg(feature = "multiplayer")] + { + match self.widget_host.ev_loop() { + ButtonEvent::LeftClick => { + if let Some(index) = self.selected { + let file_name = self.list.get(index+self.page).unwrap(); + if let Some(scr) = read_map(&file_name) { + return GameEvent::ChangeScreen(Screen::Build(scr.with_file_name(&file_name).as_host())); + } + } + } + _ => () + } + } + match self.widget_new.ev_loop() { + ButtonEvent::LeftClick => { + + }, + _ => () + } + match self.widget_play.ev_loop() { + ButtonEvent::LeftClick => { + if let Some(index) = self.selected { + let file_name = self.list.get(index+self.page).unwrap(); + if let Some(scr) = read_map(&file_name) { + return GameEvent::ChangeScreen(Screen::Build(scr.with_file_name(&file_name))); + } + } + } + _ => () + } + + for (i, slot) in self.level_select_widgets.iter_mut().enumerate() { + match slot.ev_loop() { + ButtonEvent::LeftClick => { + if let Some(sel) = self.selected && sel == i+self.page { + // already selected + // deselect + self.selected = None; + } else { + // user selected this item + self.selected = Some(i + self.page); + } + slot.set_selected(false); + }, + _ => () + } + } + if let Some(selection) = self.selected { + for (i, slot) in self.level_select_widgets.iter_mut().enumerate() { + if i == selection { + slot.set_selected(true); + } + } + } + + // return to welcome screen + if is_key_pressed(KeyCode::Q) || is_key_pressed(KeyCode::Escape) { + return GameEvent::ChangeScreen(Screen::Welcome(WelcomeScreen::new())) + } + + if is_key_pressed(KeyCode::Down) { + if self.page < self.list.len() - self.level_select_widgets.len() { + self.page += 1; + + // TODO: update widget text + for (i, slot) in self.level_select_widgets.iter_mut().enumerate() { + if let Some(text) = self.list.get(i+self.page) { + slot.set_visibility(true); + slot.set_text(text); + } else { + slot.set_visibility(false); + } + } + } + } + if is_key_pressed(KeyCode::Up) { + if self.page > 0 { + self.page -= 1; + + + // TODO: update widget text + for (i, slot) in self.level_select_widgets.iter_mut().enumerate() { + if let Some(text) = self.list.get(i+self.page) { + slot.set_visibility(true); + slot.set_text(text); + } else { + slot.set_visibility(false); + } + } + } + } + + GameEvent::None + } +} diff --git a/src/screens/mod.rs b/src/screens/mod.rs index 87999c8..43b31f2 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -1,9 +1,11 @@ mod welcome; -mod build; +pub mod build; mod inventory; +mod map_select; use welcome::WelcomeScreen; use build::BuildScreen; +use map_select::SelectScreen; use crate::types::{ GameComponent, @@ -14,18 +16,21 @@ use crate::textures::AssetStore; pub enum Screen { Welcome(WelcomeScreen), Build(BuildScreen), + Select(SelectScreen) } impl GameComponent for Screen { async fn draw(&self, assets: &AssetStore) { match self { Screen::Welcome(w) => w.draw(&assets).await, Screen::Build(b) => b.draw(&assets).await, + Screen::Select(s) => s.draw(&assets).await } } fn ev_loop(&mut self) -> GameEvent { match self { Screen::Welcome(w) => w.ev_loop(), Screen::Build(b) => b.ev_loop(), + Screen::Select(s) => s.ev_loop() } } } diff --git a/src/screens/welcome.rs b/src/screens/welcome.rs index ed3cbf4..97cc234 100644 --- a/src/screens/welcome.rs +++ b/src/screens/welcome.rs @@ -5,70 +5,84 @@ use crate::types::{ }; use crate::textures::AssetStore; use crate::draw::{ - draw_wide_button, - draw_centered_text + TextButton, + CenteredText, + Widget, + ButtonEvent }; +use super::map_select::SelectScreen; +use super::build::BuildScreen; +use super::Screen; /// The welcome screen -pub struct WelcomeScreen; +pub struct WelcomeScreen { + /// the text widget + /// used to render the welcom text + widget_title: CenteredText, + /// level select button widget + widget_select: TextButton, + /// join-host button widget + widget_join: TextButton, + /// game quit button widget + widget_quit: TextButton, +} impl WelcomeScreen { /// create a new WelcomeScreen instance pub fn new() -> Self { - Self + Self { + widget_title: CenteredText::new("LittleTown", 0.5,0.45) + .with_font_size(100), + widget_select: TextButton::new("Maps", 0.35, 0.55) + .with_font_size(40), + widget_join: TextButton::new("Join", 0.5, 0.55) + .with_font_size(40), + widget_quit: TextButton::new("Quit", 0.65, 0.55) + .with_font_size(40) + } } } impl GameComponent for WelcomeScreen { async fn draw(&self, assets: &AssetStore) { - draw_centered_text( - "Welcome", - screen_width()/2.0, screen_height()/2.0 - 60.0, - Some(assets.font), 100, WHITE); - draw_wide_button( - "Build", false, - screen_width()/2.0, screen_height()/2.0 + 0.0, - &assets); - draw_wide_button( - "Host", false, - screen_width()/2.0, screen_height()/2.0 + 60.0, - &assets); - draw_wide_button( - "Join", false, - screen_width()/2.0, screen_height()/2.0 + 120.0, - &assets); + { + // draw background image + let dim: f32 = if screen_width() < screen_height() { + screen_width() * 2.0/3.0 + } else { screen_height() * 2.0/3.0 }; + draw_texture_ex( + assets.icon, + screen_width()/2.0 - dim/2.0, + screen_height()/2.0 - dim/2.0, + WHITE, + DrawTextureParams { + dest_size: Some(Vec2::new(dim, dim)), + ..Default::default() + } + ); + draw_rectangle(0.0, 0.0, screen_width(), screen_height(), Color::from_rgba(0, 0, 0, 100)); + } + + + self.widget_title.draw(&assets).await; - draw_wide_button( - "Quit", false, - screen_width()/2.0, screen_height()/2.0 + 180.0, - &assets); + self.widget_select.draw(&assets).await; + self.widget_join.draw(&assets).await; + self.widget_quit.draw(&assets).await; } fn ev_loop(&mut self) -> GameEvent { - if is_mouse_button_pressed(MouseButton::Left) { - let (x,y) = mouse_position(); - let sw_2 = screen_width() / 2.0; - let sh_2 = screen_height() / 2.0; - let btn_x_offset = sw_2 - 190.0 / 2.0; - let btn_height = 49.0; + self.widget_title.ev_loop(); - if x >= btn_x_offset && x <= btn_x_offset + 190.0 { - // might be pressing a button - if y >= sh_2 + 00.0 && y <= sh_2 + 0.0 + btn_height { - // pressed build - return GameEvent::ChangeScreen(crate::Screen::Build(super::build::BuildScreen::new())); - } - if y >= sh_2 + 60.0 && y <= sh_2 + 60.0 + btn_height { - // pressed host - return GameEvent::ChangeScreen(crate::Screen::Build(super::build::BuildScreen::multiplayer(true))); - } - if y >= sh_2 + 120.0 && y <= sh_2 + 120.0 + btn_height { - // pressed Join - return GameEvent::ChangeScreen(crate::Screen::Build(super::build::BuildScreen::multiplayer(false))); - } - if y >= sh_2 + 180.0 && y <= sh_2 + 180.0 + btn_height { - // pressed quit - return GameEvent::Quit; - } - } + match self.widget_select.ev_loop() { + ButtonEvent::LeftClick => return GameEvent::ChangeScreen(Screen::Select(SelectScreen::new())), + _ => () + } + match self.widget_join.ev_loop() { + ButtonEvent::LeftClick => return GameEvent::ChangeScreen(Screen::Build(BuildScreen::multiplayer())), + _ => () + } + match self.widget_quit.ev_loop() { + ButtonEvent::LeftClick => return GameEvent::Quit, + _ => () } GameEvent::None diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..cb5e072 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,94 @@ +use directories_next::ProjectDirs; +use std::path::Path; +use std::fs::{ + File, + OpenOptions, + read_dir +}; +use crate::screens::build::BuildScreen; +use std::io::{ + BufReader, + Read, + Write +}; +use nanoserde::{DeJson, SerJson}; + +/// returns the OS specific data dir as a string +/// if there is no such dir, None will be returned +fn get_data_dir() -> Option { + if let Some(pd) = ProjectDirs::from("icu", "ccw", "little_town") { + if let Some(path) = pd.data_dir().to_str() { + return Some(path.to_string()); + } + } + + None +} + +/// returns a list of all world file names +pub fn get_world_list() -> Vec { + if let Some(dir) = get_data_dir() { + if let Ok(rd) = &mut read_dir(dir) { + let mut builder = Vec::new(); + + while let Some(entry) = rd.next() { + if let Ok(entry) = entry { + if let Ok(ftype) = entry.file_type() && ftype.is_file() { + if let Some(name) = entry.file_name().to_str() { + builder.push(name.to_string()); + } + } + } + } + + return builder; + } + } + Vec::new() +} + +/// tries loading a game state from a file +pub fn read_map(name: &str) -> Option { + let existing_maps = get_world_list(); + if let Some(dir) = get_data_dir() && existing_maps.contains(&name.to_string()) { + if let Ok(file) = File::open(Path::new(&dir).join(name)) { + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + if buf_reader.read_to_string(&mut contents).is_ok() { + match BuildScreen::deserialize_json(&contents) { + Ok(bscr) => return Some(bscr), + Err(e) => println!("{:?}", e) + } + } else { + println!("Faield to read file"); + } + } else { + println!("Failed to open file"); + } + } else { + println!("World not found"); + } + + None +} + +/// tries saving the map to disk +pub fn save_map(name: &str, bscr: &BuildScreen) { + if let Some(dir) = get_data_dir() { + let json = BuildScreen::serialize_json(&bscr); + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(Path::new(&dir).join(name)); + + if let Ok(file) = &mut file { + if file.write_all(json.as_bytes()).is_err() { + println!("Failed to write file"); + }; + } else { + println!("Failed to open file") + } + } +} diff --git a/src/textures.rs b/src/textures.rs index 16076b9..22b8380 100644 --- a/src/textures.rs +++ b/src/textures.rs @@ -4,6 +4,7 @@ use crate::types::Direction; /// Collection of textures required by game pub struct AssetStore { pub font: Font, + pub icon: Texture2D, pub ui: UIAssetCollection, } pub struct UIAssetCollection { @@ -31,7 +32,16 @@ impl DirectionalTexture { } } } - +macro_rules! include_img_asset { + ($name:literal) => { + Texture2D::from_image( + &Image::from_file_with_format( + include_bytes!(concat!("../assets/", $name)), + Some(ImageFormat::Png) + ) + ) + }; +} macro_rules! include_base_tile { ($name:literal, $dir:literal) => { Texture2D::from_image( @@ -65,6 +75,7 @@ impl AssetStore { /// loads all the textures pub async fn init() -> Self { Self { + icon: include_img_asset!("icon.png"), font: load_ttf_font_from_bytes(include_bytes!("../assets/fonts/Fonts/Kenney Pixel.ttf")).expect("Failed to load font"), ui: UIAssetCollection { long_button: (include_ui_img!("buttonLong_brown"), -- 2.38.5