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

Implement save file system

parent 691b6c08
No related branches found
No related tags found
No related merge requests found
Pipeline #241 passed with stages
in 4 minutes and 40 seconds
......@@ -1330,6 +1330,26 @@ dependencies = [
"parking_lot_core 0.9.5",
]
[[package]]
name = "directories"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "discard"
version = "1.0.4"
......@@ -1601,6 +1621,7 @@ dependencies = [
"bevy",
"bevy_ecs_tilemap",
"bevy_tweening",
"directories",
"fake",
"fastrand",
"iyes_loopless",
......@@ -3118,6 +3139,17 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"redox_syscall",
"thiserror",
]
[[package]]
name = "regex"
version = "1.7.0"
......
......@@ -32,6 +32,7 @@ ldtk_rust = "0.6.0"
kayak_ui.workspace = true
kayak_font.workspace = true
fake = "2.5.0"
directories = "4.0.1"
#remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
......
#![feature(result_option_inspect)]
pub mod assets;
pub mod graphics;
pub mod multiplayer;
pub mod persistance;
pub mod splash_screen;
pub mod states;
pub mod system;
......
......@@ -23,5 +23,6 @@ fn main() {
.add_plugin(game_core::graphics::GraphicsPlugin)
.add_plugins(micro_banimate::BanimatePluginGroup)
.add_plugins(AdventUIPlugins)
.add_plugin(game_core::persistance::PersistencePlugin)
.run();
}
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::{LoadFileEvent, SaveFileEvent};
use crate::system::flow::AppState;
pub struct PersistencePlugin;
impl Plugin for PersistencePlugin {
fn build(&self, app: &mut App) {
app.add_event::<SaveFileEvent>()
.add_event::<LoadFileEvent>()
.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(),
);
}
}
}
pub use __plugin::PersistencePlugin;
pub use save_file::{LoadFileEvent, PersistenceState, SaveFileEvent};
use std::fs;
use std::io::Write;
use std::ops::Deref;
use std::path::Path;
use bevy::math::Vec3;
use bevy::prelude::*;
use bevy::utils::HashMap;
use serde::{Deserialize, Serialize};
use crate::states::Player;
use crate::world::{
CurrentResidence, EncounterState, HungerState, TradingState, TravelPath, TravelTarget,
};
#[derive(Serialize, Deserialize, Debug, Resource)]
pub struct PersistenceState {
pub player_location: Vec3,
pub player_inventory: TradingState,
pub player_hunger: HungerState,
pub travel_path: Option<TravelPath>,
pub travel_target: Option<TravelTarget>,
pub previous_location: CurrentResidence,
pub encounter_state: EncounterState,
pub town_states: HashMap<String, (TradingState, HungerState)>,
}
#[derive(Clone, Debug)]
pub struct SaveFileEvent {
pub filename: Option<String>,
}
#[derive(Clone, Debug)]
pub struct LoadFileEvent {
pub filename: Option<String>,
}
pub fn sync_state_to_persistence(
mut commands: Commands,
mut state: Option<ResMut<PersistenceState>>,
player_query: Query<
(
&Transform,
Option<&TravelPath>,
Option<&TravelTarget>,
&CurrentResidence,
),
With<Player>,
>,
encounters: Res<EncounterState>,
hunger: Option<Res<HungerState>>,
trading: Option<Res<TradingState>>,
) {
match state {
Some(mut state) => {
if let Some((transform, maybe_travel, maybe_target, residence)) =
player_query.iter().next()
{
*state = PersistenceState {
player_location: transform.translation,
player_inventory: trading.map(|r| r.clone()).unwrap_or_default(),
player_hunger: hunger.map(|r| r.clone()).unwrap_or_default(),
travel_path: maybe_travel.cloned(),
travel_target: maybe_target.cloned(),
previous_location: residence.clone(),
encounter_state: encounters.clone(),
town_states: Default::default(),
};
}
}
None => {
if let Some((transform, maybe_travel, maybe_target, residence)) =
player_query.iter().next()
{
commands.insert_resource(PersistenceState {
player_location: transform.translation,
player_inventory: trading.map(|r| r.clone()).unwrap_or_default(),
player_hunger: hunger.map(|r| r.clone()).unwrap_or_default(),
travel_path: maybe_travel.cloned(),
travel_target: maybe_target.cloned(),
previous_location: residence.clone(),
encounter_state: encounters.clone(),
town_states: Default::default(),
})
}
}
}
}
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");
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");
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"),
}) {
Ok(file) => {
serde_json::to_writer_pretty(file, &*state)
.inspect_err(|err| {
log::error!("{}", err);
})
.expect("Failed to create save data");
}
Err(e) => {
log::error!("{}", e);
}
}
}
}
}
use std::time::Duration;
use bevy::prelude::Component;
use micro_musicbox::prelude::{AudioEasing, AudioTween, MusicBox};
use crate::assets::AssetHandles;
#[derive(Component, Debug, Default, Copy, Clone)]
pub struct Player;
pub fn on_enter_game(mut musicbox: MusicBox<AssetHandles>) {
musicbox.fade_in_music(
"bgm",
AudioTween::new(Duration::from_secs(2), AudioEasing::Linear),
);
}
......@@ -17,11 +17,6 @@ pub fn spawn_menu_entities(
assets: Res<AssetHandles>,
mut musicbox: MusicBox<AssetHandles>,
) {
musicbox.fade_in_music(
"bgm",
AudioTween::new(Duration::from_secs(2), AudioEasing::Linear),
);
commands.spawn((
SpriteBundle {
texture: assets.image("menu_background"),
......
......@@ -12,6 +12,7 @@ impl Plugin for StatesPlugin {
fn build(&self, app: &mut App) {
app.add_enter_system(AppState::Menu, menu_state::spawn_menu_entities)
.add_exit_system(AppState::Menu, menu_state::despawn_menu_entities)
.add_enter_system(AppState::InGame, game_state::on_enter_game)
.add_system_set(
ConditionSet::new()
.run_in_state(AppState::Menu)
......
use bevy::prelude::*;
use kayak_ui::prelude::*;
use kayak_ui::widgets::{KImage, KImageBundle, TextureAtlasBundle, TextureAtlasProps};
use serde::{Deserialize, Serialize};
use crate::assets::AssetHandles;
use crate::parent_widget;
use crate::ui::prelude::*;
#[derive(Clone, Default, Eq, PartialEq)]
#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum IconContent {
Image(String),
Atlas(String, usize),
......
......@@ -5,6 +5,7 @@ use fake::faker::lorem::en::{Paragraph, Paragraphs};
use fake::Fake;
use ldtk_rust::{EntityInstance, FieldInstance};
use num_traits::AsPrimitive;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::states::Player;
......@@ -114,7 +115,7 @@ impl From<Vec<EncounterZone>> for WorldZones {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EncounterOutcome {
GainResource {
resource_type: String,
......@@ -133,13 +134,13 @@ pub enum EncounterOutcome {
},
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EncounterOption {
pub label: String,
pub outcome: Vec<EncounterOutcome>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Encounter {
pub title: String,
pub description: String,
......@@ -185,7 +186,7 @@ pub fn gen_encounter() -> Encounter {
}
}
#[derive(Clone, Default, Resource, Debug)]
#[derive(Clone, Default, Resource, Debug, Serialize, Deserialize)]
pub enum EncounterState {
#[default]
NoEncounter,
......
......@@ -10,6 +10,7 @@ mod encounters;
mod generators;
mod spawning;
mod towns;
mod trading;
mod travel;
mod utils;
mod world_query;
......@@ -20,6 +21,7 @@ impl Plugin for WorldPlugin {
app.init_resource::<TownPaths>()
.init_resource::<WorldZones>()
.init_resource::<EncounterState>()
.add_event::<PopulateWorldEvent>()
.add_enter_system(AppState::InGame, |mut commands: Commands| {
commands.insert_resource(ActiveLevel::new("Grantswaith"));
})
......@@ -28,6 +30,7 @@ impl Plugin for WorldPlugin {
.run_in_state(AppState::InGame)
// .with_system(debug::create_tombstones)
.with_system(spawning::spawn_world_data)
.with_system(spawning::populate_world)
.with_system(travel::tick_travelling_merchant)
.with_system(encounters::notify_new_zone)
......@@ -38,4 +41,7 @@ impl Plugin for WorldPlugin {
pub use encounters::{EncounterState, WorldZones};
pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget};
pub use trading::{HungerState, ItemName, TradeGood, TradingState};
pub use world_query::{CameraBounds, MapQuery};
use crate::world::spawning::PopulateWorldEvent;
......@@ -5,12 +5,17 @@ use num_traits::AsPrimitive;
use serde_json::Value;
use crate::assets::{AssetHandles, LdtkProject, LevelIndex, TilesetIndex};
use crate::persistance::PersistenceState;
use crate::states::Player;
use crate::system::camera::ChaseCam;
use crate::world::encounters::WorldZones;
use crate::world::towns::{CurrentResidence, TownPaths};
use crate::world::utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32};
use crate::world::world_query::MapQuery;
use crate::world::{TravelPath, TravelTarget};
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
pub struct PopulateWorldEvent;
pub fn spawn_world_data(
mut commands: Commands,
......@@ -20,6 +25,7 @@ pub fn spawn_world_data(
level_index: Res<LevelIndex>,
tileset_index: Res<TilesetIndex>,
mut last_spawned_level: Local<String>,
mut events: EventWriter<PopulateWorldEvent>,
) {
let mut active_level = match active_level {
Some(l) => l,
......@@ -149,41 +155,116 @@ pub fn spawn_world_data(
));
});
events.send(PopulateWorldEvent);
}
}
#[derive(Resource)]
pub struct PendingLoadState(PersistenceState);
pub fn populate_world(
mut commands: Commands,
mut events: ResMut<Events<PopulateWorldEvent>>,
mut active_level: Option<ResMut<ActiveLevel>>,
assets: Res<AssetHandles>,
level_index: Res<LevelIndex>,
existing_player: Query<Entity, With<Player>>,
pending_load: Option<Res<PendingLoadState>>,
) {
let should_populate = !events
.drain()
.collect::<Vec<PopulateWorldEvent>>()
.is_empty();
if should_populate {
let mut active_level = match active_level {
Some(l) => l,
None => return,
};
let level = match level_index.get(&active_level.map) {
Some(l) => l,
None => return,
};
let trade_routes = TownPaths::from(MapQuery::get_entities_of(level));
let random_start = trade_routes.routes.get(&String::from("The Royal Lampoon"));
if let Some(start) = random_start {
if let Some(route) = start.routes.values().next() {
let point = route.nodes[0];
commands.spawn((
SpriteSheetBundle {
transform: Transform::from_xyz(
grid_to_px(point.tile_x),
level.px_hei as f32 - grid_to_px(point.tile_y),
400.0,
),
texture_atlas: assets.atlas("icons"),
sprite: TextureAtlasSprite {
index: 0,
..Default::default()
},
let start = trade_routes
.routes
.get(&String::from("The Royal Lampoon"))
.unwrap();
if let Some(route) = start.routes.values().next() {
let point = route.nodes[0];
let mut cmds = commands.spawn((
SpriteSheetBundle {
transform: pending_load
.as_ref()
.map(|pending| Transform::from_translation(pending.0.player_location))
.unwrap_or_else(|| {
Transform::from_xyz(
grid_to_px(point.tile_x),
level.px_hei as f32 - grid_to_px(point.tile_y),
400.0,
)
}),
texture_atlas: assets.atlas("icons"),
sprite: TextureAtlasSprite {
index: 0,
..Default::default()
},
start
.create_route_bundle_for(
start
.routes
.keys()
.nth(fastrand::usize(0..start.routes.len()))
.cloned()
.unwrap(),
level,
)
.unwrap(),
Player,
ChaseCam,
));
..Default::default()
},
start
.create_route_bundle_for(
start
.routes
.keys()
.nth(fastrand::usize(0..start.routes.len()))
.cloned()
.unwrap(),
level,
)
.unwrap(),
Player,
ChaseCam,
));
match &pending_load {
Some(ref state) => {
match (
&state.0.previous_location,
&state.0.travel_path,
&state.0.travel_target,
) {
(res, Some(path), Some(target)) => {
cmds.insert((res.clone(), path.clone(), target.clone()));
}
(res, None, Some(target)) => {
cmds.insert((res.clone(), target.clone()));
}
(res, Some(path), None) => {
cmds.insert((res.clone(), path.clone()));
}
(res, None, None) => {
cmds.insert(res.clone());
}
}
}
None => {
if let Some(bundle) = start.create_route_bundle_for(
start
.routes
.keys()
.nth(fastrand::usize(0..start.routes.len()))
.cloned()
.unwrap(),
level,
) {
cmds.insert(bundle);
}
}
}
}
......@@ -191,5 +272,20 @@ pub fn spawn_world_data(
commands.insert_resource(trade_routes);
commands.insert_resource(world_zones);
commands.insert_resource(
pending_load
.as_ref()
.map(|pending| &pending.0.player_inventory)
.cloned()
.unwrap_or_default(),
);
commands.insert_resource(
pending_load
.as_ref()
.map(|pending| &pending.0.player_hunger)
.cloned()
.unwrap_or_default(),
);
}
}
......@@ -11,7 +11,7 @@ pub type Town = String;
/// Stores the ID of the most recent town that has been inhabited,
/// as well as the current travel state
#[derive(Component)]
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub enum CurrentResidence {
TravellingFrom(String),
RestingAt(String),
......@@ -31,10 +31,10 @@ impl CurrentResidence {
}
}
#[derive(Component)]
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct TravelTarget(pub String);
#[derive(Component)]
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct TravelPath {
pub path: Vec<Vec2>,
last_index: usize,
......
use bevy::prelude::Resource;
use bevy::utils::hashbrown::hash_map::Entry;
use bevy::utils::HashMap;
use num_traits::AsPrimitive;
use serde::{Deserialize, Serialize};
use crate::ui::components::IconContent;
pub type ItemName = String;
#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize)]
pub struct TradingState {
pub items: HashMap<ItemName, usize>,
pub gold: isize,
}
#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize)]
pub struct HungerState {
pub sustenance: usize,
pub starvation_ticks: f32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct TradeGood {
pub name: String,
pub icon: IconContent,
}
impl TradingState {
pub fn spend_gold(&mut self, amount: impl AsPrimitive<isize>) -> bool {
if self.gold < amount.as_() {
false
} else {
self.adjust_gold(amount);
true
}
}
pub fn adjust_gold(&mut self, adjustment: impl AsPrimitive<isize>) {
self.gold += adjustment.as_();
}
pub fn remove_items(
&mut self,
identifier: impl ToString,
amount: impl AsPrimitive<usize>,
) -> bool {
match self.items.entry(identifier.to_string()) {
Entry::Occupied(mut e) => {
let amount = amount.as_();
if e.get() >= &amount {
*e.get_mut() -= amount;
if e.get() == &0 {
e.remove();
}
true
} else {
false
}
}
Entry::Vacant(_) => false,
}
}
pub fn add_items(&mut self, identifier: impl ToString, amount: impl AsPrimitive<usize>) {
*self.items.entry(identifier.to_string()).or_insert(0) += amount.as_()
}
pub fn try_buy_items(
&mut self,
cost: impl AsPrimitive<isize>,
identifier: impl ToString,
amount: impl AsPrimitive<usize>,
) -> bool {
if self.spend_gold(cost) {
self.add_items(identifier, amount);
true
} else {
false
}
}
pub fn try_sell_items(
&mut self,
value: impl AsPrimitive<isize>,
identifier: impl ToString,
amount: impl AsPrimitive<usize>,
) -> bool {
if self.remove_items(identifier, amount) {
self.adjust_gold(value);
true
} else {
false
}
}
}
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