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