diff --git a/crates/energy/src/electromagnetism/fields.rs b/crates/energy/src/electromagnetism/fields.rs index 3d72ceb..241cfc3 100644 --- a/crates/energy/src/electromagnetism/fields.rs +++ b/crates/energy/src/electromagnetism/fields.rs @@ -88,7 +88,9 @@ impl MagneticField { // Biot-Savart law: dB = (μ₀/4π) * (I dl × r̂) / r² let field_magnitude = MAGNETIC_CONSTANT_DIV_4PI * current / (distance * distance); - let field = Vec2::new(-r_unit.y, r_unit.x) * current_direction.length() * field_magnitude; + let field = Vec2::new(-r_unit.y, r_unit.x) + * (current_direction.x * r_unit.y - current_direction.y * r_unit.x) + * field_magnitude; Self::new(field, field_position) } diff --git a/crates/systems/ai/src/core/actions.rs b/crates/systems/ai/src/core/actions.rs index 8a4e08f..96d6e61 100644 --- a/crates/systems/ai/src/core/actions.rs +++ b/crates/systems/ai/src/core/actions.rs @@ -8,7 +8,7 @@ use std::sync::Arc; /// of the Thinker that spawned it, and the actual Action system executing the /// Action itself. #[derive(Debug, Clone, Component, Eq, PartialEq, Reflect)] -#[component(storage = "SparseSet")] +#[component(storage = "Table")] pub enum ActionState { /// Initial state. No action should be performed. Init, diff --git a/crates/systems/ai/src/drives/mod.rs b/crates/systems/ai/src/drives/mod.rs index 1f2f38b..9acf492 100644 --- a/crates/systems/ai/src/drives/mod.rs +++ b/crates/systems/ai/src/drives/mod.rs @@ -7,7 +7,9 @@ use bevy::prelude::*; pub struct DrivesPlugin; impl Plugin for DrivesPlugin { - fn build(&self, _app: &mut App) { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::(); // Simple plugin - just makes drives available // Systems will be added later when we have proper integration } diff --git a/crates/systems/ai/src/drives/needs.rs b/crates/systems/ai/src/drives/needs.rs index 40a7c55..7947171 100644 --- a/crates/systems/ai/src/drives/needs.rs +++ b/crates/systems/ai/src/drives/needs.rs @@ -2,7 +2,7 @@ use crate::prelude::*; use bevy::prelude::*; /// Core need types representing basic drives -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] pub enum NeedType { Hunger, // Need for nourishment Safety, // Need to avoid danger @@ -11,7 +11,7 @@ pub enum NeedType { } /// Component representing a single need -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, Reflect)] pub struct Need { /// Type of need pub need_type: NeedType, diff --git a/crates/systems/save_system/src/lib.rs b/crates/systems/save_system/src/lib.rs index 9898993..9c55579 100644 --- a/crates/systems/save_system/src/lib.rs +++ b/crates/systems/save_system/src/lib.rs @@ -3,20 +3,29 @@ use bevy::prelude::*; pub mod save_system; pub mod versioning; -/// Plugin for save/load functionality with versioning support -#[derive(Default)] pub struct SaveSystemPlugin; impl Plugin for SaveSystemPlugin { - fn build(&self, _app: &mut App) { - // TODO: Will add save/load systems when needed - // For now, just register the plugin + fn build(&self, app: &mut App) { + app.init_resource::() + .register_type::() + .register_type::() + .register_type::() + .register_type::(); + } +} + +impl Default for SaveSystemPlugin { + fn default() -> Self { + Self } } -/// Prelude for easy importing pub mod prelude { pub use super::SaveSystemPlugin; - pub use crate::save_system::{load, save}; + pub use crate::save_system::{ + get_save_directory, get_save_path, load, load_game_data, save, save_game_data, GameEvent, + GameSaveData, GameSnapshot, GameState, GameTracker, SaveMetadata, Saveable, WorldSaveExt, + }; pub use crate::versioning::{is_save_up_to_date, upgrade_save, SAVE_VERSION}; } diff --git a/crates/systems/save_system/src/save_system.rs b/crates/systems/save_system/src/save_system.rs index f26b520..a14c762 100644 --- a/crates/systems/save_system/src/save_system.rs +++ b/crates/systems/save_system/src/save_system.rs @@ -1,22 +1,79 @@ use crate::versioning::{is_save_up_to_date, upgrade_save}; +use bevy::prelude::ReflectComponent; +use bevy::prelude::*; +use bevy::reflect::{Reflect, ReflectSerialize}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashMap; use std::fs; +use std::path::PathBuf; + +/// Get the default save directory for the platform +pub fn get_save_directory() -> PathBuf { + #[cfg(target_os = "windows")] + { + std::env::var("APPDATA") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join("LP") + } + + #[cfg(target_os = "macos")] + { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(".")) + .join("Library") + .join("Application Support") + .join("LP") + } + + #[cfg(target_os = "linux")] + { + std::env::var("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|_| { + std::env::var("HOME").map(|home| PathBuf::from(home).join(".local").join("share")) + }) + .unwrap_or_else(|_| PathBuf::from(".")) + .join("LP") + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + PathBuf::from(".") + } +} + +/// Get the full path for a save file +pub fn get_save_path(filename: &str) -> PathBuf { + let save_dir = get_save_directory(); + + // Ensure save directory exists + if let Err(e) = fs::create_dir_all(&save_dir) { + eprintln!( + "Warning: Could not create save directory {:?}: {}", + save_dir, e + ); + return PathBuf::from(filename); // Fallback to current directory + } + + save_dir.join(filename) +} -/// Saves data to a file pub fn save(data: &T, path: &str) -> Result<(), String> { + let full_path = get_save_path(path); let json = serde_json::to_string_pretty(data).map_err(|e| format!("Serialization failed: {}", e))?; - fs::write(path, json).map_err(|e| format!("File write failed: {}", e))?; + fs::write(&full_path, json).map_err(|e| format!("File write failed: {}", e))?; Ok(()) } -/// Loads data from a file, or returns a properly initialized default save if missing pub fn load Deserialize<'de> + Default + Serialize>(path: &str) -> Result { - let json = match fs::read_to_string(path) { + let full_path = get_save_path(path); + let json = match fs::read_to_string(&full_path) { Ok(content) => content, Err(_) => { - eprintln!("[Warning] Save file not found. Creating a new one."); let default_data = T::default(); if let Err(e) = save(&default_data, path) { @@ -38,3 +95,308 @@ pub fn load Deserialize<'de> + Default + Serialize>(path: &str) -> R serde_json::from_value(data).map_err(|e| format!("Final deserialization failed: {}", e)) } + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct GameSaveData { + pub version: String, + pub timestamp: f64, + pub game_time: f64, + pub metadata: SaveMetadata, + + pub events: Vec, + pub entities: HashMap>, + pub game_state: GameState, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Reflect)] +pub struct SaveMetadata { + pub created_at: String, + pub game_version: String, + pub save_system_version: String, + pub platform: String, + pub description: Option, + pub playtime_seconds: f64, +} + +impl Default for SaveMetadata { + fn default() -> Self { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + Self { + created_at: format!("Unix timestamp: {}", timestamp), + game_version: env!("CARGO_PKG_VERSION").to_string(), + save_system_version: crate::versioning::SAVE_VERSION.to_string(), + platform: std::env::consts::OS.to_string(), + description: None, + playtime_seconds: 0.0, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Reflect)] +pub struct GameState { + pub total_energy: f32, + pub entity_count: u32, + pub environment: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Reflect)] +pub struct GameEvent { + pub event_type: String, + pub game_time: f64, + pub entity_id: Option, + pub data: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Reflect)] +pub struct EntityData { + pub position: Option, + pub energy: Option, + pub custom: HashMap, +} + +#[derive(Component, Reflect)] +pub struct Saveable; + +#[derive(Resource, Clone)] +pub struct GameTracker { + pub state: GameState, + pub events: Vec, + pub auto_save_timer: Timer, + pub snapshots: Vec, + pub max_snapshots: usize, +} + +impl Default for GameTracker { + fn default() -> Self { + Self::new(60.0) // Auto-save every 60 seconds by default + } +} + +#[derive(Debug, Clone)] +pub struct GameSnapshot { + pub state: GameState, + pub events: Vec, + pub timestamp: f64, + pub name: String, +} + +impl GameTracker { + /// Create a new GameTracker with auto-save enabled + pub fn new(auto_save_interval_secs: f32) -> Self { + Self { + state: GameState::default(), + events: Vec::new(), + auto_save_timer: Timer::from_seconds(auto_save_interval_secs, TimerMode::Repeating), + snapshots: Vec::new(), + max_snapshots: 10, + } + } + + /// Create a snapshot of the current game state + pub fn snapshot(&mut self, name: String, timestamp: f64) { + let snapshot = GameSnapshot { + state: self.state.clone(), + events: self.events.clone(), + timestamp, + name, + }; + + self.snapshots.push(snapshot); + + // Keep only the latest snapshots + if self.snapshots.len() > self.max_snapshots { + self.snapshots.remove(0); + } + } + + /// Rollback to the most recent snapshot + pub fn rollback(&mut self) -> Result { + if let Some(snapshot) = self.snapshots.last() { + self.state = snapshot.state.clone(); + self.events = snapshot.events.clone(); + Ok(format!("Rolled back to snapshot: {}", snapshot.name)) + } else { + Err("No snapshots available for rollback".to_string()) + } + } + + /// Rollback to a specific named snapshot + pub fn rollback_to(&mut self, name: &str) -> Result { + if let Some(snapshot) = self.snapshots.iter().rev().find(|s| s.name == name) { + self.state = snapshot.state.clone(); + self.events = snapshot.events.clone(); + Ok(format!("Rolled back to snapshot: {}", name)) + } else { + Err(format!("Snapshot '{}' not found", name)) + } + } + + /// Convenience method for auto-saving + pub fn auto_save(&self, world: &mut World, game_time: f64) -> Result<(), String> { + save_game_data( + world, + self, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs_f64(), + game_time, + ) + } + + /// Update the auto-save timer and return true if it's time to save + pub fn should_auto_save(&mut self, delta_time: f32) -> bool { + self.auto_save_timer + .tick(std::time::Duration::from_secs_f32(delta_time)); + self.auto_save_timer.just_finished() + } + + pub fn set_environment(&mut self, name: String, value: f32) { + self.state.environment.insert(name, value); + } + + pub fn set_total_energy(&mut self, energy: f32) { + self.state.total_energy = energy; + } + + pub fn set_entity_count(&mut self, count: u32) { + self.state.entity_count = count; + } + + pub fn log_event( + &mut self, + event_type: String, + entity_id: Option, + data: String, + game_time: f64, + ) { + self.events.push(GameEvent { + event_type, + game_time, + entity_id, + data, + }); + + if self.events.len() > 100 { + self.events.remove(0); + } + } +} + +pub fn save_game_data( + world: &mut World, + tracker: &GameTracker, + time: f64, + game_time: f64, +) -> Result<(), String> { + let mut entities = HashMap::new(); + + let mut query = world.query_filtered::>(); + let saveable_entities: Vec = query.iter(world).collect(); + + let type_registry = world.resource::(); + let type_registry = type_registry.read(); + + for entity in saveable_entities { + let entity_id = format!("{:?}", entity); + let mut entity_data = HashMap::new(); + + if let Ok(entity_ref) = world.get_entity(entity) { + for component_id in entity_ref.archetype().components() { + if let Some(component_info) = world.components().get_info(component_id) { + if let Some(type_registration) = + type_registry.get(component_info.type_id().unwrap()) + { + if let Some(reflect_serialize) = + type_registration.data::() + { + let component_name = + type_registration.type_info().type_path().to_string(); + + if component_name.contains("Saveable") { + continue; + } + + if let Some(reflect_component) = + type_registration.data::() + { + if let Some(reflected) = reflect_component.reflect(entity_ref) { + let serializable = + reflect_serialize.get_serializable(reflected); + if let Ok(value) = serde_json::to_value(&*serializable) { + entity_data.insert(component_name, value); + } + } + } + } + } + } + } + } + + entities.insert(entity_id, entity_data); + } + + let mut metadata = SaveMetadata::default(); + metadata.playtime_seconds = game_time; + + let save_data = GameSaveData { + version: crate::versioning::SAVE_VERSION.to_string(), + timestamp: time, + game_time, + metadata, + game_state: tracker.state.clone(), + events: tracker.events.clone(), + entities, + }; + + save(&save_data, "game_save.json") +} + +pub fn load_game_data() -> Result { + load::("game_save.json") +} + +/// Extension trait for World to add bevy_save-style convenience methods +pub trait WorldSaveExt { + fn save_game(&mut self, path: &str) -> Result<(), String>; + fn load_game(&mut self, path: &str) -> Result<(), String>; +} + +impl WorldSaveExt for World { + fn save_game(&mut self, _path: &str) -> Result<(), String> { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs_f64(); + + let game_time = self + .get_resource::