move menu work, start on preliminary save system
This commit is contained in:
+4
-1
@@ -2,8 +2,11 @@ use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*};
|
||||
use main_menu::MainMenuPlugin;
|
||||
use networking::NetworkingPlugin;
|
||||
|
||||
use crate::save::SavePlugin;
|
||||
|
||||
mod main_menu;
|
||||
mod networking;
|
||||
mod save;
|
||||
|
||||
/// We target this 16:9 resolution to make things easier.
|
||||
pub const WINDOW_TARGET_WIDTH_U32: u32 = 3840;
|
||||
@@ -22,7 +25,7 @@ pub enum AppState {
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((DefaultPlugins, MainMenuPlugin, NetworkingPlugin))
|
||||
.add_plugins((DefaultPlugins, MainMenuPlugin, NetworkingPlugin, SavePlugin))
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, debug_border)
|
||||
.insert_resource(ClearColor(Color::srgb(0.07, 0.33, 0.45)))
|
||||
|
||||
+110
-19
@@ -1,4 +1,8 @@
|
||||
use crate::save::get_saves;
|
||||
use crate::{AppState, WINDOW_TARGET_HEIGHT, WINDOW_TARGET_WIDTH};
|
||||
use bevy::color::palettes::tailwind::SLATE_900;
|
||||
use bevy::input_focus::tab_navigation::TabIndex;
|
||||
use bevy::text::{EditableText, TextCursorStyle};
|
||||
use bevy::ui_widgets::Button;
|
||||
use bevy::{prelude::*, ui_widgets::Activate};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
@@ -9,6 +13,7 @@ impl Plugin for MainMenuPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::Menu), setup_menu)
|
||||
.add_systems(OnExit(AppState::Menu), cleanup_menu)
|
||||
// .add_systems(Startup, enable_ui_debug)
|
||||
.add_systems(Update, (menu_redraw).run_if(in_state(AppState::Menu)))
|
||||
.add_observer(button_activate_observer)
|
||||
.add_systems(Update, (scale_ui, style_ui_elements));
|
||||
@@ -78,6 +83,7 @@ pub enum MenuLayer {
|
||||
Root,
|
||||
Play,
|
||||
LoadSave,
|
||||
NewGame,
|
||||
Browse,
|
||||
DirectConnect,
|
||||
Settings,
|
||||
@@ -110,16 +116,31 @@ impl MenuLayer {
|
||||
menu_button("Direct Connect", Some(MenuLayer::DirectConnect)),
|
||||
]
|
||||
}),
|
||||
MenuLayer::LoadSave => Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
MenuLayer::LoadSave => {
|
||||
// TODO: this needs to be on an IO thread
|
||||
let saves = get_saves().unwrap_or_default();
|
||||
let entries: Vec<_> = saves
|
||||
.into_iter()
|
||||
.map(|m| menu_button(m.save_name, None))
|
||||
.collect();
|
||||
|
||||
Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
{entries}
|
||||
(
|
||||
menu_button("New Game", Some(MenuLayer::NewGame))
|
||||
InteractionColors::GOOD
|
||||
),
|
||||
]
|
||||
})
|
||||
}
|
||||
MenuLayer::NewGame => Box::new(bsn! {
|
||||
full_menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Save 1", None),
|
||||
menu_button("Save 2", None),
|
||||
(
|
||||
menu_button("New Game", None)
|
||||
InteractionColors::GOOD
|
||||
),
|
||||
{new_game_menu()}
|
||||
]
|
||||
}),
|
||||
MenuLayer::Browse => Box::new(bsn! {
|
||||
@@ -170,6 +191,13 @@ struct LastMenu(Vec<MenuLayer>);
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash, Deref, DerefMut)]
|
||||
struct CurrentMenu(Vec<MenuLayer>);
|
||||
|
||||
// fn enable_ui_debug(mut debug_options: ResMut<GlobalUiDebugOptions>) {
|
||||
// debug_options.enabled = true;
|
||||
// debug_options.outline_border_box = true;
|
||||
// debug_options.outline_padding_box = true;
|
||||
// debug_options.outline_content_box = true;
|
||||
// }
|
||||
|
||||
fn setup_menu(mut commands: Commands) {
|
||||
commands.insert_resource(CurrentMenu(vec![MenuLayer::Root]));
|
||||
commands.insert_resource(LastMenu(vec![]));
|
||||
@@ -233,6 +261,70 @@ fn menu_redraw(
|
||||
}
|
||||
}
|
||||
|
||||
fn new_game_menu() -> impl SceneList {
|
||||
bsn_list![
|
||||
(
|
||||
Text("New Game")
|
||||
Node {
|
||||
align_self: AlignSelf::Center,
|
||||
}
|
||||
TextFont {
|
||||
// font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: px(100.0),
|
||||
}
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||
TextShadow
|
||||
),
|
||||
(
|
||||
Text("Save Name")
|
||||
Node {
|
||||
margin: UiRect::axes(px(12), px(0)),
|
||||
}
|
||||
TextFont {
|
||||
// font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: px(66.0),
|
||||
}
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||
TextShadow
|
||||
),
|
||||
(
|
||||
#NewGameTextField
|
||||
Node {
|
||||
border: Val::all(px(6)),
|
||||
padding: Val::all(px(12)),
|
||||
border_radius: BorderRadius::all(px(12)),
|
||||
}
|
||||
// BorderColor::from(SLATE_800)
|
||||
BorderColor::from(SLATE_900)
|
||||
EditableText {
|
||||
cursor_width: 0.05,
|
||||
}
|
||||
TextLayout::no_wrap()
|
||||
TextFont {
|
||||
font_size: FontSize::Px(66.),
|
||||
}
|
||||
TextCursorStyle {
|
||||
color: Color::BLACK
|
||||
}
|
||||
TabIndex(0)
|
||||
BackgroundColor(Color::srgb(0.48, 0.84, 0.87))
|
||||
),
|
||||
(
|
||||
Node {
|
||||
margin: UiRect::top(px(48)),
|
||||
}
|
||||
Children [
|
||||
menu_button("Create", None)
|
||||
Node {
|
||||
margin: UiRect::left(Val::Auto),
|
||||
}
|
||||
InteractionColors::GOOD
|
||||
on(|event: On<Activate>, q_text:| { info!("create!") })
|
||||
]
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn menu_layer(layer: usize) -> impl Scene {
|
||||
let name = format!("MenuLayer{}", layer);
|
||||
bsn! {
|
||||
@@ -242,8 +334,6 @@ fn menu_layer(layer: usize) -> impl Scene {
|
||||
width: percent(100.0 / N_LAYERS as f32),
|
||||
height: percent(100),
|
||||
flex_direction: FlexDirection::Column,
|
||||
// align_items: AlignItems::Start,
|
||||
align_items: AlignItems::Baseline,
|
||||
justify_content: JustifyContent::FlexEnd,
|
||||
row_gap: px(100),
|
||||
}
|
||||
@@ -251,21 +341,22 @@ fn menu_layer(layer: usize) -> impl Scene {
|
||||
}
|
||||
|
||||
fn full_menu_layer(layer: usize) -> impl Scene {
|
||||
let name = format!("FullMenuLayer{}", layer);
|
||||
bsn! {
|
||||
menu_layer(layer)
|
||||
Name(name)
|
||||
Layer(layer)
|
||||
Node {
|
||||
width: Val::Auto,
|
||||
flex_direction: FlexDirection::Column,
|
||||
align_self: AlignSelf::FlexEnd,
|
||||
flex_grow: 1.,
|
||||
border_radius: BorderRadius::all(px(24)),
|
||||
padding: Val::all(px(24)),
|
||||
}
|
||||
BackgroundColor(Color::srgba(0.8, 0.1, 0.1, 0.7))
|
||||
BackgroundColor(Color::srgb(0.4, 0.4, 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_button(label: &str, next_layer: Option<MenuLayer>) -> impl Scene {
|
||||
let next_layer_component = next_layer
|
||||
.map(|n| Box::new(bsn! { NextLayer(n) }) as Box<dyn Scene>)
|
||||
.unwrap_or(Box::new(bsn! {}));
|
||||
|
||||
fn menu_button(label: impl Into<String>, next_layer: Option<MenuLayer>) -> impl Scene {
|
||||
bsn! {
|
||||
Button
|
||||
Node {
|
||||
@@ -277,7 +368,7 @@ fn menu_button(label: &str, next_layer: Option<MenuLayer>) -> impl Scene {
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect::all(px(4)),
|
||||
}
|
||||
next_layer_component
|
||||
{next_layer.map(|l| template_value(NextLayer(l)))}
|
||||
BorderColor::from(Color::BLACK)
|
||||
InteractionColors::DEFAULT
|
||||
Interaction
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use app_dirs2::{AppInfo, app_dir};
|
||||
use bevy::prelude::*;
|
||||
use jiff::Zoned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const APP_INFO: AppInfo = AppInfo {
|
||||
name: "OfficeWarper",
|
||||
author: "MarinoDev",
|
||||
};
|
||||
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ActiveGameSave {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Message, Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SaveMessage;
|
||||
|
||||
pub struct SavePlugin;
|
||||
|
||||
/// The metadata stored with each game save
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct GameSaveMetadata {
|
||||
pub save_name: String,
|
||||
pub version: String,
|
||||
pub timestamp: Zoned,
|
||||
}
|
||||
|
||||
impl Plugin for SavePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_message::<SaveMessage>()
|
||||
.add_systems(PostUpdate, handle_saves);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_saves(
|
||||
mut save_messages: MessageReader<SaveMessage>,
|
||||
active_game_save: Option<Res<ActiveGameSave>>,
|
||||
) {
|
||||
for _save in save_messages.read() {
|
||||
let Some(ref active_game_save) = active_game_save else {
|
||||
warn!("Save requested, but no active game save exists!");
|
||||
return;
|
||||
};
|
||||
|
||||
info!("Saving!");
|
||||
let save_subpath = format!("saves/{}", active_game_save.name);
|
||||
let save_path = match app_dir(app_dirs2::AppDataType::UserData, &APP_INFO, &*save_subpath) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get the save path for \"{}\": {e}",
|
||||
active_game_save.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = std::fs::write(save_path.join("version.txt"), "v0.0.1") {
|
||||
error!("Error when writing the game version: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Game saved!");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_saves() -> Result<Vec<GameSaveMetadata>, std::io::Error> {
|
||||
let saves_path = match app_dir(app_dirs2::AppDataType::UserData, &APP_INFO, "saves") {
|
||||
Ok(p) => p,
|
||||
Err(app_dirs2::AppDirsError::Io(e)) => {
|
||||
error!("Failed to get path to the saves: {e}",);
|
||||
return Err(e);
|
||||
}
|
||||
Err(app_dirs2::AppDirsError::NotSupported) => {
|
||||
error!("The app data directory is not known on this platform!");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"the app data directory is not known on this platform",
|
||||
));
|
||||
}
|
||||
Err(app_dirs2::AppDirsError::InvalidAppInfo) => {
|
||||
// :/
|
||||
panic!("APP_INFO invalid!");
|
||||
}
|
||||
};
|
||||
|
||||
let dir = match std::fs::read_dir(saves_path) {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
error!("Failed to read the saves path: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// TODO: do a meta.ron file instead of name.txt
|
||||
let save_folders = dir
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.ok()
|
||||
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
|
||||
})
|
||||
.map(|dir| dir.path());
|
||||
|
||||
let mut saves = vec![];
|
||||
for save_folder in save_folders {
|
||||
let meta_raw = match std::fs::read_to_string(save_folder.join("meta.ron")) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to read meta for save \"{save_folder:?}\"");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let meta: GameSaveMetadata = match ron::from_str(&meta_raw) {
|
||||
Err(e) => {
|
||||
error!("Error when deserializing metadata for save \"{save_folder:?}\": {e}");
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
|
||||
}
|
||||
Ok(m) => m,
|
||||
};
|
||||
saves.push(meta);
|
||||
}
|
||||
|
||||
Ok(saves)
|
||||
}
|
||||
Reference in New Issue
Block a user