move menu work, start on preliminary save system

This commit is contained in:
2026-08-21 01:20:22 -05:00
parent f4d803eb35
commit 7bf458c3cc
5 changed files with 350 additions and 21 deletions
Generated
+106
View File
@@ -184,6 +184,18 @@ dependencies = [
"libc",
]
[[package]]
name = "app_dirs2"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e"
dependencies = [
"jni 0.21.1",
"ndk-context",
"winapi",
"xdg",
]
[[package]]
name = "approx"
version = "0.5.1"
@@ -2269,6 +2281,37 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -3141,6 +3184,59 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-link",
]
[[package]]
name = "jiff-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
dependencies = [
"defmt",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jni"
version = "0.21.1"
@@ -5090,8 +5186,12 @@ dependencies = [
name = "tt_net_test_vibeless"
version = "0.1.0"
dependencies = [
"app_dirs2",
"bevy",
"derive_more",
"jiff",
"ron",
"serde",
]
[[package]]
@@ -6078,6 +6178,12 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
[[package]]
name = "xdg"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]]
name = "xkbcommon-dl"
version = "0.4.2"
+5 -1
View File
@@ -4,8 +4,12 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.19"
app_dirs2 = "2.5.5"
bevy = { version = "0.19", features = ["system_clipboard"] }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
jiff = { version = "0.2.35", features = ["serde"] }
ron = "0.12.2"
serde = { version = "1.0.229", features = ["derive"] }
[profile.dev]
opt-level = 1
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}