~comcloudway/little_town

f4b01fce86e5dcaeeb3085cf3f10ec10a5061810 — Jakob Meier 1 year, 8 months ago de32390
Added Map select screen
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"),