From 378f2fc0ea85647fad40ecddabc16dc0dfab27d4 Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Tue, 6 Dec 2022 17:40:10 +0000 Subject: [PATCH] Implement main menu through kayak, load autosaves --- game_core/src/persistance/fs_utils.rs | 17 +++ game_core/src/persistance/mod.rs | 18 +-- game_core/src/persistance/save_file.rs | 52 ++++++++- game_core/src/states/menu_state.rs | 138 +++++++++++----------- game_core/src/ui/mod.rs | 17 +-- game_core/src/ui/screens/main_menu.rs | 151 +++++++++++++++++++++++++ game_core/src/ui/screens/mod.rs | 2 + game_core/src/world/mod.rs | 1 + game_core/src/world/spawning.rs | 13 +-- game_core/src/world/travel.rs | 3 + 10 files changed, 303 insertions(+), 109 deletions(-) create mode 100644 game_core/src/persistance/fs_utils.rs create mode 100644 game_core/src/ui/screens/main_menu.rs diff --git a/game_core/src/persistance/fs_utils.rs b/game_core/src/persistance/fs_utils.rs new file mode 100644 index 0000000..2097a58 --- /dev/null +++ b/game_core/src/persistance/fs_utils.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +pub const AUTOSAVE_NAME: &str = "autosave.json"; + +pub fn get_root_save_dir() -> Option<PathBuf> { + Some( + directories::ProjectDirs::from("com", "microhacks", "TraderTales")? + .data_dir() + .to_path_buf(), + ) +} + +pub fn has_auto_save() -> bool { + get_root_save_dir() + .map(|dir: PathBuf| dir.join(AUTOSAVE_NAME).exists()) + .unwrap_or(false) +} diff --git a/game_core/src/persistance/mod.rs b/game_core/src/persistance/mod.rs index 07c7842..dd60ae0 100644 --- a/game_core/src/persistance/mod.rs +++ b/game_core/src/persistance/mod.rs @@ -1,10 +1,13 @@ +pub mod fs_utils; mod save_file; mod __plugin { use bevy::prelude::*; use iyes_loopless::prelude::ConditionSet; - use crate::persistance::save_file::{handle_save_event, sync_state_to_persistence}; + use crate::persistance::save_file::{ + handle_load_event, handle_save_event, sync_state_to_persistence, + }; use crate::persistance::{LoadFileEvent, SaveFileEvent}; use crate::system::flow::AppState; @@ -13,18 +16,17 @@ mod __plugin { fn build(&self, app: &mut App) { app.add_event::<SaveFileEvent>() .add_event::<LoadFileEvent>() + .add_system_set( + ConditionSet::new() + .run_in_state(AppState::Menu) + .with_system(handle_load_event) + .into(), + ) .add_system_set( ConditionSet::new() .run_in_state(AppState::InGame) .with_system(sync_state_to_persistence) .with_system(handle_save_event) - .with_system( - |input: Res<Input<KeyCode>>, mut events: EventWriter<SaveFileEvent>| { - if input.just_released(KeyCode::Space) { - events.send(SaveFileEvent { filename: None }); - } - }, - ) .into(), ); } diff --git a/game_core/src/persistance/save_file.rs b/game_core/src/persistance/save_file.rs index a537edd..36ce085 100644 --- a/game_core/src/persistance/save_file.rs +++ b/game_core/src/persistance/save_file.rs @@ -1,4 +1,5 @@ use std::fs; +use std::fs::File; use std::io::Write; use std::ops::Deref; use std::path::Path; @@ -6,11 +7,15 @@ use std::path::Path; use bevy::math::Vec3; use bevy::prelude::*; use bevy::utils::HashMap; +use iyes_loopless::state::NextState; use serde::{Deserialize, Serialize}; +use crate::persistance::fs_utils::{get_root_save_dir, AUTOSAVE_NAME}; use crate::states::Player; +use crate::system::flow::AppState; use crate::world::{ - CurrentResidence, EncounterState, HungerState, TradingState, TravelPath, TravelTarget, + CurrentResidence, EncounterState, HungerState, PendingLoadState, TradingState, TravelPath, + TravelTarget, }; #[derive(Serialize, Deserialize, Debug, Resource)] @@ -30,11 +35,23 @@ pub struct SaveFileEvent { pub filename: Option<String>, } +impl SaveFileEvent { + pub fn autosave() -> Self { + Self { filename: None } + } +} + #[derive(Clone, Debug)] pub struct LoadFileEvent { pub filename: Option<String>, } +impl LoadFileEvent { + pub fn autosave() -> Self { + Self { filename: None } + } +} + pub fn sync_state_to_persistence( mut commands: Commands, mut state: Option<ResMut<PersistenceState>>, @@ -91,16 +108,15 @@ pub fn handle_save_event( mut events: ResMut<Events<SaveFileEvent>>, state: Option<Res<PersistenceState>>, ) { - let root_data_dir = directories::ProjectDirs::from("com", "microhacks", "TraderTales") - .expect("Failed to get project dir"); + let root_data_dir = get_root_save_dir().expect("Could not find root dir for saves"); if let Some(state) = state { for event in events.drain() { - std::fs::create_dir_all(root_data_dir.data_dir()).expect("Failed to create data dir"); + std::fs::create_dir_all(&root_data_dir).expect("Failed to create data dir"); match fs::File::create(match event.filename { - Some(name) => root_data_dir.data_dir().join(name), - None => root_data_dir.data_dir().join("autosave.json"), + Some(name) => root_data_dir.join(name), + None => root_data_dir.join(AUTOSAVE_NAME), }) { Ok(file) => { serde_json::to_writer_pretty(file, &*state) @@ -116,3 +132,27 @@ pub fn handle_save_event( } } } + +pub fn handle_load_event(mut commands: Commands, mut events: ResMut<Events<LoadFileEvent>>) { + let root_dir = get_root_save_dir().expect("Could not find root dir for saves"); + for event in events.drain() { + log::info!("Trying to load file"); + let path = root_dir.join(event.filename.as_deref().unwrap_or(AUTOSAVE_NAME)); + log::info!("Path is {}", path.display()); + match std::fs::File::open(path) { + Ok(file) => match serde_json::from_reader::<File, PersistenceState>(file) { + Ok(value) => { + log::info!("GET PER: {:?}", &value); + commands.insert_resource(PendingLoadState(value)); + commands.insert_resource(NextState(AppState::InGame)); + } + Err(e) => { + log::error!("Failed to parse save file: {}", e); + } + }, + Err(e) => { + log::error!("Failed to load file: {}", e); + } + } + } +} diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs index bc37c79..6dc2ed5 100644 --- a/game_core/src/states/menu_state.rs +++ b/game_core/src/states/menu_state.rs @@ -17,75 +17,75 @@ pub fn spawn_menu_entities( assets: Res<AssetHandles>, mut musicbox: MusicBox<AssetHandles>, ) { - commands.spawn(( - SpriteBundle { - texture: assets.image("menu_background"), - transform: Transform::from_scale(Vec3::splat(0.85)), - ..Default::default() - }, - MenuStateEntity, - )); - - commands - .spawn(( - MenuStateEntity, - NodeBundle { - style: Style { - size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - ..Default::default() - }, - ..Default::default() - }, - )) - .with_children(|commands| { - commands.spawn(TextBundle { - text: Text::from_section( - "Trader Tales", - TextStyle { - font_size: 72.0, - font: assets.font("compass_pro"), - color: Color::ANTIQUE_WHITE, - }, - ), - style: Style { - margin: UiRect::top(Val::Percent(20.0)), - ..Default::default() - }, - ..Default::default() - }); - commands.spawn(( - TextBundle { - text: Text::from_section( - "> Press Space <", - TextStyle { - font_size: 48.0, - font: assets.font("compass_pro"), - color: Color::ANTIQUE_WHITE, - }, - ), - style: Style { - margin: UiRect::top(Val::Px(50.0)), - ..Default::default() - }, - ..Default::default() - }, - Animator::new( - Tween::new( - EaseFunction::QuadraticInOut, - Duration::from_secs(1), - TextColorLens { - start: Color::ANTIQUE_WHITE, - end: *Color::ANTIQUE_WHITE.set_a(0.0), - section: 0, - }, - ) - .with_repeat_count(RepeatCount::Infinite) - .with_repeat_strategy(RepeatStrategy::MirroredRepeat), - ), - )); - }); + // commands.spawn(( + // SpriteBundle { + // texture: assets.image("menu_background"), + // transform: Transform::from_scale(Vec3::splat(0.85)), + // ..Default::default() + // }, + // MenuStateEntity, + // )); + // + // commands + // .spawn(( + // MenuStateEntity, + // NodeBundle { + // style: Style { + // size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), + // flex_direction: FlexDirection::Column, + // align_items: AlignItems::Center, + // ..Default::default() + // }, + // ..Default::default() + // }, + // )) + // .with_children(|commands| { + // commands.spawn(TextBundle { + // text: Text::from_section( + // "Trader Tales", + // TextStyle { + // font_size: 72.0, + // font: assets.font("compass_pro"), + // color: Color::ANTIQUE_WHITE, + // }, + // ), + // style: Style { + // margin: UiRect::top(Val::Percent(20.0)), + // ..Default::default() + // }, + // ..Default::default() + // }); + // commands.spawn(( + // TextBundle { + // text: Text::from_section( + // "> Press Space <", + // TextStyle { + // font_size: 48.0, + // font: assets.font("compass_pro"), + // color: Color::ANTIQUE_WHITE, + // }, + // ), + // style: Style { + // margin: UiRect::top(Val::Px(50.0)), + // ..Default::default() + // }, + // ..Default::default() + // }, + // Animator::new( + // Tween::new( + // EaseFunction::QuadraticInOut, + // Duration::from_secs(1), + // TextColorLens { + // start: Color::ANTIQUE_WHITE, + // end: *Color::ANTIQUE_WHITE.set_a(0.0), + // section: 0, + // }, + // ) + // .with_repeat_count(RepeatCount::Infinite) + // .with_repeat_strategy(RepeatStrategy::MirroredRepeat), + // ), + // )); + // }); } pub fn go_to_game(input: Res<Input<KeyCode>>, mut commands: Commands) { diff --git a/game_core/src/ui/mod.rs b/game_core/src/ui/mod.rs index 8d2581b..50ed8ea 100644 --- a/game_core/src/ui/mod.rs +++ b/game_core/src/ui/mod.rs @@ -91,21 +91,10 @@ mod _config { pub struct ConfigureKayakPlugin; impl Plugin for ConfigureKayakPlugin { fn build(&self, app: &mut App) { - app - // .init_resource::<MenuInterface>() - // .add_system(cursor_animation_system) - .add_exit_system(AppState::Setup, configure_kayak_ui) - // .add_enter_system( - // AppState::Menu, - // super::screens::main_menu::main_menu::render_main_menu, - // ) - // .add_system_set( - // ConditionSet::new() - // .run_in_state(AppState::Menu) - // .with_system(main_menu_router) - // .into(), - // ) + app.add_exit_system(AppState::Setup, configure_kayak_ui) + .add_enter_system(AppState::Menu, super::screens::render_menu_ui) .add_enter_system(AppState::InGame, super::screens::render_in_game_ui) + .add_exit_system(AppState::InGame, remove_ui) .add_exit_system(AppState::Menu, remove_ui); } } diff --git a/game_core/src/ui/screens/main_menu.rs b/game_core/src/ui/screens/main_menu.rs new file mode 100644 index 0000000..ed2f0c3 --- /dev/null +++ b/game_core/src/ui/screens/main_menu.rs @@ -0,0 +1,151 @@ +use bevy::prelude::*; +use iyes_loopless::prelude::NextState; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{ + ElementBundle, KImage, KImageBundle, KayakAppBundle, TextProps, TextWidgetBundle, +}; + +use crate::assets::AssetHandles; +use crate::persistance::{fs_utils, LoadFileEvent}; +use crate::system::flow::AppState; +use crate::ui::components::*; +use crate::ui::prelude::{pct, px, stretch, value}; +use crate::ui::sync::UITravelInfo; +use crate::ui::utilities::context::create_root_context; +use crate::ui::utilities::StateUIRoot; +use crate::ui::widgets::*; +use crate::world::EncounterState; +use crate::{ + empty_props, on_button_click, parent_widget, register_widget, + register_widget_with_many_resources, register_widget_with_resource, +}; + +empty_props!(MainMenuProps); +parent_widget!(MainMenuProps => MainMenuLayout); + +pub fn render_main_menu_layout( + In((widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + assets: Res<AssetHandles>, +) -> bool { + let parent_id = Some(entity); + let has_autosave = fs_utils::has_auto_save(); + + let root_styles = KStyle { + ..Default::default() + }; + + let image_styles = KStyle { + position_type: value(KPositionType::SelfDirected), + ..Default::default() + }; + + let contents_container_style = KStyle { + position_type: value(KPositionType::SelfDirected), + width: pct(65.0), + height: pct(80.0), + min_width: px(400.0), + min_height: px(300.0), + max_width: px(500.0), + max_height: px(400.0), + top: stretch(1.0), + left: stretch(1.0), + right: stretch(1.0), + bottom: stretch(1.0), + layout_type: value(LayoutType::Column), + row_between: px(20.0), + ..Default::default() + }; + + let header_style = KStyle { + left: stretch(1.0), + right: stretch(1.0), + ..Default::default() + }; + + let image = KImage(assets.image("menu_background")); + + let button_style = KStyle { + width: px(300.0), + left: stretch(1.0), + right: stretch(1.0), + ..Default::default() + }; + + let on_new = on_button_click!(Commands, |mut commands: Commands| { + commands.insert_resource(NextState(AppState::InGame)); + }); + + let on_continue = on_button_click!(EventWriter<LoadFileEvent>, |mut events: EventWriter< + LoadFileEvent, + >| { + log::info!("SENDING LOAD FILE EVENT"); + events.send(LoadFileEvent::autosave()); + }); + + rsx! { + <ElementBundle styles={root_styles}> + <KImageBundle styles={image_styles} image={image} /> + <ElementBundle styles={contents_container_style}> + <TextWidgetBundle + text={TextProps { + content: String::from("Trader Tales"), + font: Some(String::from("header")), + size: 72.0, + ..Default::default() + }} + styles={header_style} + /> + + <ButtonWidget + styles={button_style.clone()} + props={ButtonWidgetProps { + text: String::from("New Game"), + font_size: 32.0, + ..Default::default() + }} + on_event={on_new} + /> + + <ButtonWidget + styles={button_style.clone()} + props={ButtonWidgetProps { + text: String::from("Continue Game"), + font_size: 32.0, + is_disabled: !has_autosave, + ..Default::default() + }} + on_event={on_continue} + /> + </ElementBundle> + </ElementBundle> + } + + true +} + +pub fn render_menu_ui(mut commands: Commands) { + let parent_id = None; + let mut widget_context = create_main_menu_context(); + + rsx! { + <KayakAppBundle> + <MainMenuLayout /> + </KayakAppBundle> + } + + commands.spawn((UICameraBundle::new(widget_context), StateUIRoot)); +} + +fn create_main_menu_context() -> KayakRootContext { + let mut widget_context = create_root_context(); + + register_widget!( + widget_context, + MainMenuProps, + EmptyState, + render_main_menu_layout + ); + + widget_context +} diff --git a/game_core/src/ui/screens/mod.rs b/game_core/src/ui/screens/mod.rs index 2cce8e4..9f3b28a 100644 --- a/game_core/src/ui/screens/mod.rs +++ b/game_core/src/ui/screens/mod.rs @@ -1,3 +1,5 @@ mod in_game; +mod main_menu; pub use in_game::render_in_game_ui; +pub use main_menu::render_menu_ui; diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index addfc26..a0e75b1 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -40,6 +40,7 @@ impl Plugin for WorldPlugin { } pub use encounters::{EncounterState, WorldZones}; +pub use spawning::PendingLoadState; pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget}; pub use trading::{HungerState, ItemName, TradeGood, TradingState}; pub use world_query::{CameraBounds, MapQuery}; diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs index 8ffe127..4e59f6a 100644 --- a/game_core/src/world/spawning.rs +++ b/game_core/src/world/spawning.rs @@ -160,7 +160,7 @@ pub fn spawn_world_data( } #[derive(Resource)] -pub struct PendingLoadState(PersistenceState); +pub struct PendingLoadState(pub PersistenceState); pub fn populate_world( mut commands: Commands, @@ -216,17 +216,6 @@ pub fn populate_world( }, ..Default::default() }, - start - .create_route_bundle_for( - start - .routes - .keys() - .nth(fastrand::usize(0..start.routes.len())) - .cloned() - .unwrap(), - level, - ) - .unwrap(), Player, ChaseCam, )); diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs index 10441bb..03a1e8b 100644 --- a/game_core/src/world/travel.rs +++ b/game_core/src/world/travel.rs @@ -3,6 +3,7 @@ use std::ops::SubAssign; use bevy::math::Vec3Swizzles; use bevy::prelude::*; +use crate::persistance::SaveFileEvent; use crate::world::encounters::EncounterState; use crate::world::towns::{CurrentResidence, TravelPath, TravelTarget}; use crate::world::PathingResult; @@ -18,6 +19,7 @@ pub fn tick_travelling_merchant( &mut CurrentResidence, )>, encounter_state: Res<EncounterState>, + mut autosave: EventWriter<SaveFileEvent>, ) { if encounter_state.is_in_encounter() { return; @@ -34,6 +36,7 @@ pub fn tick_travelling_merchant( .entity(entity) .remove::<(TravelPath, TravelTarget)>(); *residence = CurrentResidence::RestingAt(target.0.clone()); + autosave.send(SaveFileEvent::autosave()); } PathingResult::NextNode => { path.increment_indexes(); -- GitLab