Skip to content
Snippets Groups Projects
Verified Commit 378f2fc0 authored by Louis's avatar Louis :fire:
Browse files

Implement main menu through kayak, load autosaves

parent 6e4e566e
No related branches found
No related tags found
No related merge requests found
Pipeline #242 passed with stages
in 3 minutes and 36 seconds
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)
}
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(),
);
}
......
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);
}
}
}
}
......@@ -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) {
......
......@@ -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);
}
}
......
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
}
mod in_game;
mod main_menu;
pub use in_game::render_in_game_ui;
pub use main_menu::render_menu_ui;
......@@ -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};
......
......@@ -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,
));
......
......@@ -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();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment