M Cargo.lock => Cargo.lock +34 -1
@@ 714,6 714,27 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 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",
@@ 2527,6 2549,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml => Cargo.toml +2 -1
@@ 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 <comcloudway@ccw.icu>" ]
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 = [
M README.md => README.md +30 -0
@@ 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 `<Escape>` |
+
+#### Block orientations
+| Action | Key(s) / MouseButton |
+|--------|----------------------|
+| North | `K` |
+| South | `J` |
+| West | `H` |
+| East | `L` ||
+
+#### Camera movement
+| Action | Key(s) / MouseButton |
+|--------|----------------------|
+| Up/North | `<ArrowUp>` |
+| Down/South | `<ArrowDown>` |
+| Left | `<ArrowLeft>` |
+| Right | `<ArrowRight>` |
+
+
+### Inventory
+
+### Level Select screen
+
## Building
We are currently building binaries
for some distros using the Sourcehut CI.
R logo.png => assets/icon.png +0 -0
M src/draw.rs => src/draw.rs +404 -37
@@ 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>, 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<u16>,
+ /// the font to use
+ /// defaults to macroquad default font
+ font: Option<Font>,
+ /// text color
+ /// defaults to white
+ color: Option<Color>
+}
+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<Font>,
+ /// custom font color
+ font_color: Option<Color>,
+ /// custom font size to use
+ font_size: Option<u16>,
+ /// 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<Font>,
+ /// custom font color
+ /// defaults to white
+ font_color: Option<Color>,
+ /// custom font size to use
+ font_size: Option<u16>,
+ /// 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
+
+ }
}
M src/main.rs => src/main.rs +2 -0
@@ 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;
M src/screens/build.rs => src/screens/build.rs +41 -30
@@ 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<Peer>
+ multiplayer: Option<Peer>,
+ #[nserde(skip)]
+ file_name: Option<String>
}
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()));
}
}
M src/screens/inventory.rs => src/screens/inventory.rs +45 -11
@@ 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>, 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(
A src/screens/map_select.rs => src/screens/map_select.rs +211 -0
@@ 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<String>,
+ /// 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<SelectableText>,
+ /// currently selected slot
+ selected: Option<usize>,
+ /// 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
+ }
+}
M src/screens/mod.rs => src/screens/mod.rs +6 -1
@@ 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()
}
}
}
M src/screens/welcome.rs => src/screens/welcome.rs +63 -49
@@ 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
A src/storage.rs => src/storage.rs +94 -0
@@ 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<String> {
+ 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<String> {
+ 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<BuildScreen> {
+ 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")
+ }
+ }
+}
M src/textures.rs => src/textures.rs +12 -1
@@ 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"),