diff --git a/src/features/mod.rs b/src/features/mod.rs index ea4140a..3cd19db 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -19,3 +19,4 @@ pub use pom::PomPlugin; pub use savegame::SavegamePlugin; pub use start_screen::StartScreenPlugin; pub use status::StatusPlugin; +pub use ui::UiPlugin; diff --git a/src/features/savegame/components.rs b/src/features/savegame/components.rs index be3791f..c8690ae 100644 --- a/src/features/savegame/components.rs +++ b/src/features/savegame/components.rs @@ -1,11 +1,30 @@ use crate::prelude::*; +use std::fs; use std::path::PathBuf; -#[derive(Resource)] +#[derive(Resource, Clone, Debug)] pub struct SavegamePath(pub PathBuf); +#[derive(Debug)] +pub struct SavegameInfo { + pub path: SavegamePath, + pub index: u32, + pub total_berries: u32, + pub completed_focus: u32, +} + +#[derive(Deserialize)] +struct PartialSaveData { + session_tracker: PartialSessionTracker, +} + +#[derive(Deserialize)] +struct PartialSessionTracker { + completed_focus_phases: u32, +} + impl SavegamePath { - pub fn new(name: &str) -> Self { + pub fn new(index: u32) -> Self { let base_path = get_internal_path().unwrap_or_else(|| { println!( "Could not determine platform-specific save directory. Falling back to `./saves/" @@ -17,6 +36,59 @@ impl SavegamePath { panic!("Failed to create save directory at {:?}: {}", base_path, e); } - Self(base_path.join(name)) + Self(base_path.join(format!("savegame-{}.json", index))) + } + + pub fn list() -> Vec { + let mut savegames = Vec::new(); + + let Some(base_path) = get_internal_path() else { + return Vec::new(); + }; + if !base_path.exists() { + return Vec::new(); + } + let Ok(entries) = fs::read_dir(base_path) else { + return Vec::new(); + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) != Some("json") { + continue; + } + let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) else { + continue; + }; + if !file_name.starts_with("savegame-") { + continue; + } + let Ok(index) = file_name.trim_start_matches("savegame-").parse::() else { + continue; + }; + let Ok(content) = fs::read_to_string(&path) else { + continue; + }; + let Ok(data) = serde_json::from_str::(&content) else { + continue; + }; + + savegames.push(SavegameInfo { + path: SavegamePath(path), + index, + total_berries: 0, // TODO: add total_berries + completed_focus: data.session_tracker.completed_focus_phases, + }); + } + + savegames.sort_by_key(|s| s.index); + savegames + } + + pub fn next() -> Self { + let savegames = Self::list(); + let next_index = savegames.last().map(|s| s.index + 1).unwrap_or(0); + Self::new(next_index) } } diff --git a/src/features/savegame/mod.rs b/src/features/savegame/mod.rs index c76362d..d52d6b6 100644 --- a/src/features/savegame/mod.rs +++ b/src/features/savegame/mod.rs @@ -98,7 +98,7 @@ fn load_savegame( mut phase: ResMut, mut tracker: ResMut, mut settings: ResMut, - mut pom_query: Query<&mut GridPosition, With>, + mut pom_query: Query<(&mut GridPosition, &mut Transform), With>, ) { for _ in messages.read() { if let Ok(mut file) = File::open(&save_path.0) { @@ -114,8 +114,15 @@ fn load_savegame( *tracker = save_data.session_tracker; *settings = save_data.timer_settings; - if let Ok(mut pom_pos) = pom_query.single_mut() { + if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() { *pom_pos = save_data.pom_position; + pom_transform.translation = grid_to_world_coords( + save_data.pom_position.x, + save_data.pom_position.y, + Some(1.0), + save_data.grid_width, + save_data.grid_height, + ) } for x in 0..save_data.grid_width { diff --git a/src/features/start_screen/components.rs b/src/features/start_screen/components.rs new file mode 100644 index 0000000..abf57d5 --- /dev/null +++ b/src/features/start_screen/components.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; + +#[derive(Component)] +pub enum RootMarker { + MainMenu, + PopupSavegameLoad, +} + +#[derive(Component)] +pub enum ButtonType { + LoadGame, + NewGame, + Settings, + PopupSavegameLoad { savegame_path: SavegamePath }, + PopupClose, +} diff --git a/src/features/start_screen/consts.rs b/src/features/start_screen/consts.rs new file mode 100644 index 0000000..55073bc --- /dev/null +++ b/src/features/start_screen/consts.rs @@ -0,0 +1,5 @@ +use crate::prelude::*; + +pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); +pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); +pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); diff --git a/src/features/start_screen/mod.rs b/src/features/start_screen/mod.rs index 9be4c8b..da4c7ac 100644 --- a/src/features/start_screen/mod.rs +++ b/src/features/start_screen/mod.rs @@ -1,4 +1,9 @@ use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*}; +use components::*; +use consts::*; + +pub mod components; +pub mod consts; pub struct StartScreenPlugin; @@ -10,107 +15,198 @@ impl Plugin for StartScreenPlugin { } } -#[derive(Resource)] -struct MenuData { - button_entity: Entity, -} - -#[derive(Component)] -enum ButtonType { - LoadGame, - NewGame, - Settings, -} - -const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); -const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); -const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); - fn setup(mut commands: Commands) { - let button_entity = commands + commands.spawn(( + RootMarker::MainMenu, + Node { + width: percent(100), + height: percent(100), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + flex_direction: FlexDirection::Column, + ..default() + }, + children![ + ( + Text::new("Pomonon Garten"), + TextFont::from_font_size(64.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + ), + ( + Button, + ButtonType::LoadGame, + Node { + width: px(300), + height: px(65), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Spiel laden"), + TextFont::from_font_size(33.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + )] + ), + ( + Button, + ButtonType::NewGame, + Node { + width: px(300), + height: px(65), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Neues Spiel"), + TextFont::from_font_size(33.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + )] + ), + ( + Button, + ButtonType::Settings, + Node { + width: px(300), + height: px(65), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Einstellungen"), + TextFont::from_font_size(33.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + )] + ) + ], + )); +} + +fn spawn_load_popup(commands: &mut Commands) { + commands .spawn(( + RootMarker::PopupSavegameLoad, Node { + position_type: PositionType::Absolute, width: percent(100), height: percent(100), justify_content: JustifyContent::Center, align_items: AlignItems::Center, - flex_direction: FlexDirection::Column, ..default() }, - children![ - ( - Text::new("Pomonon Garten"), - TextFont { - font_size: 64.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)) - ), - ( - Button, - ButtonType::LoadGame, - Node { - width: px(300), - height: px(65), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(NORMAL_BUTTON), - children![( - Text::new("Spiel laden"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)) - )] - ), - ( - Button, - ButtonType::NewGame, - Node { - width: px(300), - height: px(65), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(NORMAL_BUTTON), - children![( - Text::new("Neues Spiel"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)) - )] - ), - ( - Button, - ButtonType::Settings, - Node { - width: px(300), - height: px(65), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(NORMAL_BUTTON), - children![( - Text::new("Einstellungen"), - TextFont { - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)) - )] - ) - ], + ZIndex(1), + BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)), )) - .id(); + .with_children(|parent| { + parent + .spawn(( + Node { + width: px(600.0), + height: px(500.0), + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + padding: UiRect::all(px(20.0)), + ..default() + }, + BackgroundColor(Color::srgb(0.2, 0.2, 0.2)), + BorderRadius::all(Val::Px(10.0)), + )) + .with_children(|parent| { + parent.spawn(( + Node { + width: percent(100.0), + justify_content: JustifyContent::SpaceBetween, + align_items: AlignItems::Center, + margin: UiRect::bottom(px(20.0)), + ..default() + }, + children![ + ( + Text::new("Spielstand Auswahl"), + TextFont::from_font_size(40.0), + TextColor(Color::WHITE), + ), + ( + Button, + ButtonType::PopupClose, + Node { + width: px(40.0), + height: px(40.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(Color::srgb(0.8, 0.2, 0.2)), + children![( + Text::new("X"), + TextFont::from_font_size(24.0), + TextColor(Color::WHITE), + )] + ) + ], + )); - commands.insert_resource(MenuData { button_entity }); + parent + .spawn(Node { + width: percent(100), + flex_direction: FlexDirection::Column, + overflow: Overflow::scroll_y(), + margin: UiRect::all(px(20.0)), + row_gap: px(10.0), + ..default() + }) + .with_children(|parent| { + for savegame in SavegamePath::list() { + parent.spawn(( + Button, + ButtonType::PopupSavegameLoad { + savegame_path: savegame.path.clone(), + }, + Node { + width: percent(100), + height: px(80), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(NORMAL_BUTTON), + children![( + Node { + width: percent(100), + height: percent(100), + flex_direction: FlexDirection::Column, + ..default() + }, + children![ + ( + Text::new(format!( + "Spielstand {}", + savegame.index + 1 + )), + TextFont::from_font_size(24.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + ), + ( + Text::new(format!( + "Beeren: {}, Fokusphasen abgeschlossen: {}", + savegame.total_berries, + savegame.completed_focus + )), + TextFont::from_font_size(18.0), + TextColor(Color::srgb(0.9, 0.9, 0.9)) + ) + ] + )], + )); + } + }); + }); + }); } fn menu( @@ -120,7 +216,8 @@ fn menu( (&Interaction, &ButtonType, &mut BackgroundColor), (Changed, With