diff --git a/bin/editor.rs b/bin/editor.rs index 40be78d..e44ee0e 100644 --- a/bin/editor.rs +++ b/bin/editor.rs @@ -23,15 +23,13 @@ // * make min/max/close buttons into actions not selects // * (???) Better handle hide/close monologue -use bevy::{ - asset::{Asset, Assets, ChangeWatcher}, - gltf::Gltf, - prelude::*, - utils::Duration, -}; +use bevy::{asset::ChangeWatcher, gltf::Gltf, prelude::*, utils::Duration}; use monologue_trees::{ debug::*, - editor::{assets::*, audio::*, font::*, monologue::*, *}, + editor::{ + animation::*, asset_sync::*, assets::*, audio::*, camera::*, font::*, gltf::*, level::*, + lighting::*, monologue::*, quit::*, reset::*, scene::*, timeline::*, *, + }, ui, }; @@ -142,8 +140,6 @@ fn main() { export_level.run_if(ui::activated::), rehydrate_level::, rehydrate_level::, PlaybackSettings>, - //rehydrate_level::, AnimationPlayer>, - //rehydrate_level::, ), ) .add_systems( @@ -193,13 +189,6 @@ fn main() { #[derive(Debug, Component)] pub struct TabRoot; -#[derive(Debug, Component, Reflect, Default)] -#[reflect(Component)] -pub struct LevelRoot; - -#[derive(Debug, Component)] -pub struct EditorCamera; - fn initialize_ui(mut commands: Commands) { // Empty entity for populating the level being edited commands.spawn((SpatialBundle { ..default() }, LevelRoot)); @@ -566,1075 +555,3 @@ fn spawn_tab_container( .id(), ) } - -use gltf::*; -mod gltf { - use super::*; - - #[derive(Debug, Component, Default)] - pub struct GltfWidget; - - pub fn gltf_ui( - mut events: EventReader>, - mut commands: Commands, - widget: Query>, - current: Query<(Entity, &ui::TargetAsset)>, - server: Res, - ) { - events.iter().for_each(|event| match event { - AssetEvent::Created { handle } => { - info!("Asset created! {:?}", event); - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - get_asset_name(&server, handle.clone()), - None, - ); - } - AssetEvent::Removed { handle } => { - info!("Asset removed! {:?}", event); - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - } - AssetEvent::Modified { handle } => { - info!("Asset modified! {:?}", event); - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - get_asset_name(&server, handle.clone()), - None, - ); - } - }); - } - - pub fn control_active_gltf( - events: Query>, Added)>, - root: Query>, - mut commands: Commands, - ) { - events.iter().for_each(|_| { - root.iter().for_each(|entity| { - commands.entity(entity).despawn_descendants(); - }); - }); - } -} - -use scenes::*; -mod scenes { - use super::*; - - #[derive(Debug, Component, Default)] - pub struct SceneWidget; - - pub fn add_scenes_ui( - gltf_selected: Query<&ui::TargetAsset, Added>, - mut commands: Commands, - gltfs: Res>, - widget: Query>, - ) { - gltf_selected.iter().for_each(|ui::TargetAsset { handle }| { - if let Some(gltf) = gltfs.get(&handle.clone()) { - gltf.named_scenes.iter().for_each(|(name, handle)| { - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - name.clone(), - None, - ); - }) - } - }); - } - - pub fn remove_scenes_ui( - mut gltf_unselected: RemovedComponents, - target_assets: Query<&ui::TargetAsset>, - current: Query<(Entity, &ui::TargetAsset)>, - gltfs: Res>, - mut commands: Commands, - ) { - gltf_unselected - .iter() - .filter_map(|entity| target_assets.get(entity).ok()) - .filter_map(|ui::TargetAsset { handle }| gltfs.get(handle)) - .for_each(|gltf| { - gltf.scenes.iter().for_each(|handle| { - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - }); - }); - } - - pub fn control_active_scenes( - added: Query, Added)>, - mut removed: RemovedComponents, - scene_refs: Query<&ui::TargetAsset>, - scenes: Query<(Entity, &Handle)>, - level_root: Query>, - mut commands: Commands, - ) { - // A scene button was marked inactive - removed.iter().for_each(|entity| { - // Get the handle associated with that button - scene_refs - .get(entity) - .iter() - .for_each(|ui::TargetAsset { handle }| { - scenes - .iter() - .find_map(|(entity, this_handle)| (this_handle == handle).then_some(entity)) - .iter() - .for_each(|&entity| { - commands.entity(entity).despawn_recursive(); - }); - }); - }); - added.iter().for_each(|entity| { - scene_refs - .get(entity) - .iter() - .for_each(|ui::TargetAsset { handle }| { - info!("Spawning Scene {:?}", handle); - commands - .entity(level_root.single()) - .with_children(|parent| { - parent.spawn(SceneBundle { - scene: handle.clone(), - ..default() - }); - }); - }); - }); - } -} - -use animations::*; -mod animations { - use super::*; - - #[derive(Debug, Component, Default)] - pub struct AnimationWidget; - - #[derive(Debug, Component)] - pub struct AnimationPlayAll; - - pub fn init_animations_ui( - events: Query>, - mut commands: Commands, - ) { - events.iter().for_each(|entity| { - commands.entity(entity).with_children(|parent| { - parent.spawn(( - AnimationPlayAll, - ButtonBundle { - style: Style { - border: UiRect::all(Val::Px(1.0)), - margin: UiRect::all(Val::Px(1.0)), - padding: UiRect::all(Val::Px(1.0)), - ..default() - }, - border_color: Color::BLACK.into(), - ..default() - }, - ui::Title { - text: "Play All".into(), - ..default() - }, - )); - }); - }) - } - - /// When a new scene is loaded, add any newly compatible animations - /// TODO: Add target entity(s) too - pub fn add_animations_ui( - player_spawned: Query<&Name, Added>, - widget: Query>, - mut commands: Commands, - gltfs: Res>, - clips: Res>, - ) { - player_spawned.iter().for_each(|player_name| { - gltfs - .iter() - .flat_map(|(_, gltf)| gltf.named_animations.iter()) - .filter_map(|(clip_name, handle)| { - clips.get(&handle).map(|clip| (clip_name, handle, clip)) - }) - .filter(|(_, _, clip)| clip.compatible_with(player_name)) - .for_each(|(clip_name, handle, _)| { - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - clip_name.clone(), - None, - ); - }); - }); - } - - // When a scene is de-selected, remove any outdated animation options - pub fn remove_animations_ui( - mut removed_players: RemovedComponents>, - current: Query<(Entity, &ui::TargetAsset)>, - clips: Res>, - targets: Query<(&AnimationPlayer, &Name)>, - mut commands: Commands, - ) { - // For each removed scene - removed_players.iter().for_each(|_| { - // Iterate over the current animation buttons - current - .iter() - .filter(|(_, ui::TargetAsset { handle })| { - // Check if this clip is compatible with any remaining entities - // NOTE: We are checking this is *not* compatible with any entities - clips - .get(handle) - .map(|clip| !(targets.iter().any(|(_, name)| clip.compatible_with(name)))) - .unwrap_or(true) - }) - .for_each(|(_, ui::TargetAsset { handle })| { - // Destroy the buton if it is so - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - }); - }); - } - - pub fn play_all_animations( - start: Query, Added)>, - mut stop: RemovedComponents, - play_all_btn: Query>, - clip_btns: Query>>, - mut commands: Commands, - ) { - stop.iter() - .filter(|&entity| play_all_btn.contains(entity)) - .for_each(|_| { - clip_btns.iter().for_each(|entity| { - commands.entity(entity).remove::(); - }) - }); - start - .iter() - .filter(|&entity| play_all_btn.contains(entity)) - .for_each(|_| { - clip_btns.iter().for_each(|entity| { - commands.entity(entity).insert(ui::Active); - }) - }); - } - - pub fn play_animation( - start: Query, Added)>, - mut stop: RemovedComponents, - clip_refs: Query<&ui::TargetAsset>, - mut targets: Query<(&mut AnimationPlayer, &Name), With>, - clips: Res>, - ) { - stop.iter().for_each(|entity| { - if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) { - let clip = clips.get(&handle).expect("Load animation clip"); - targets - .iter_mut() - .filter(|(_, name)| clip.compatible_with(name)) - .for_each(|(mut player, _)| { - player.pause(); - }) - } - }); - start.iter().for_each(|entity| { - if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) { - let clip = clips.get(&handle).expect("Load animation clip"); - targets - .iter_mut() - .filter(|(_, name)| clip.compatible_with(name)) - .for_each(|(mut player, _)| { - if player.is_paused() { - player.resume(); - } else { - player.play(handle.clone()).repeat(); - } - }) - } - }); - } -} - -use cameras::*; -mod cameras { - use super::*; - - #[derive(Debug, Component, Default)] - pub struct CameraWidget; - - pub fn cameras_ui( - mut added: Query<(Entity, &mut Camera, &Name), Added>, - mut removed: RemovedComponents, - editor_camera: Query>, - widget: Query>, - current: Query<(Entity, &ui::TargetEntity)>, - mut commands: Commands, - ) { - removed.iter().for_each(|entity| { - info!("Destroy button for {:?}", entity); - destroy_entity_button(¤t, &mut commands, &ui::TargetEntity { entity }); - }); - added.iter_mut().for_each(|(entity, mut camera, name)| { - info!("Camera added {:?} {:?}", entity, name); - create_entity_button( - &widget, - &mut commands, - ui::TargetEntity { entity }, - name.as_str().into(), - ); - camera.is_active = entity == editor_camera.single(); - }); - } - - /// Set the camera active component based on button clicks - pub fn manage_active_camera( - events: Query<&ui::TargetEntity, Added>, - mut cameras: Query<(Entity, &mut Camera)>, - ) { - events.iter().for_each(|ui::TargetEntity { entity }| { - cameras.iter_mut().for_each(|(this_entity, mut camera)| { - if this_entity == *entity { - info!("Marking {:?} as active camera", entity); - camera.is_active = true; - } else { - info!("Marking {:?} as inactive camera", entity); - camera.is_active = false; - } - }); - }); - } - - // In the event that an active camera is despawned, fall back to the editor camera - pub fn fallback_camera( - modified: Query, Without)>, - mut removed: RemovedComponents, - other_cameras: Query<&Camera, Without>, - mut editor_camera: Query<&mut Camera, With>, - ) { - // Any time a camera is modified - modified.iter().chain(removed.iter()).for_each(|_| { - // If no other cameras are active - if !other_cameras.iter().any(|camera| camera.is_active) { - // Make the editor camera active - editor_camera.single_mut().is_active = true; - } - }) - } -} - -use lighting::*; -mod lighting { - use super::*; - - pub fn spot_light_force_shadows(mut spot_lights: Query<&mut SpotLight, Added>) { - spot_lights.iter_mut().for_each(|mut light| { - light.shadows_enabled = true; - }) - } - - pub fn directional_light_force_shadows( - mut directional_lights: Query<&mut DirectionalLight, Added>, - ) { - directional_lights.iter_mut().for_each(|mut light| { - light.shadows_enabled = true; - }) - } - - pub fn point_light_force_shadows(mut point_lights: Query<&mut PointLight, Added>) { - point_lights.iter_mut().for_each(|mut light| { - light.shadows_enabled = true; - }) - } -} - -use reset::*; -mod reset { - use super::*; - - #[derive(Debug, Component)] - pub struct ClearLevel; - - pub fn clear_level( - events: Query, Added)>, - actives: Query< - Entity, - ( - With, - Or<( - With, - With>, - With>, - With>, - With>, - With>, - With>, - )>, - ), - >, - root: Query>, - mut commands: Commands, - ) { - events.iter().for_each(|_| { - actives.iter().for_each(|entity| { - commands.entity(entity).remove::(); - }); - root.iter().for_each(|entity| { - commands.entity(entity).despawn_descendants(); - }); - }) - } - - #[derive(Debug, Component)] - pub struct ClearAssets; - - pub fn clear_assets( - asset_holders: Query< - Entity, - Or<( - With>, - With>, - With>, - With>, - With>, - With>, - With>, - With>, - With>, - With>, - With>, - With>, - )>, - >, - mut registry: ResMut, - mut commands: Commands, - ) { - info!("Clearing assets"); - - // Clear buttons holding asset references - asset_holders - .iter() - .for_each(|entity| commands.entity(entity).despawn_recursive()); - - // Empty asset registry - registry.0.clear(); - } -} - -pub use level::*; -mod level { - use bevy::tasks::IoTaskPool; - - use super::*; - - pub type Level = DynamicScene; - - #[derive(Debug, Component, Default)] - pub struct LevelWidget; - - #[derive(Debug, Component)] - pub struct ExportLevel; - - pub fn level_ui( - mut events: EventReader>, - mut commands: Commands, - widget: Query>, - current: Query<(Entity, &ui::TargetAsset)>, - server: Res, - ) { - events.iter().for_each(|event| match event { - AssetEvent::Created { handle } => { - info!("Asset created! {:?}", event); - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - get_asset_name(&server, handle.clone()), - None, - ); - } - AssetEvent::Removed { handle } => { - info!("Asset removed! {:?}", event); - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - } - AssetEvent::Modified { handle } => { - info!("Asset modified! {:?}", event); - destroy_asset_button( - ¤t, - &mut commands, - &ui::TargetAsset { - handle: handle.clone(), - }, - ); - create_asset_button( - &widget, - &mut commands, - ui::TargetAsset { - handle: handle.clone(), - }, - get_asset_name(&server, handle.clone()), - None, - ); - } - }); - } - - pub fn load_level( - events: Query< - &ui::TargetAsset, - (Added, With>), - >, - root: Query>, - mut commands: Commands, - ) { - events.iter().for_each(|ui::TargetAsset { handle }| { - root.iter().for_each(|entity| { - commands.entity(entity).despawn_recursive(); - }); - commands.spawn(DynamicSceneBundle { - scene: handle.clone(), - ..default() - }); - }); - } - - pub fn export_level( - level_root: Query>, - audio_root: Query>, - children: Query<&Children>, - world: &World, - ) { - let app_type_registry = world.resource::().clone(); - let mut builder = DynamicSceneBuilder::from_world(world.clone()); - - builder.deny_all_resources(); - - // Exclude computed visibility - builder.deny_all(); - - // Level administrivia - builder.allow::(); - builder.allow::(); - - // TODO: Serialize Timeline - - // Scene components - builder.allow::>(); - - // Spatial components - builder.allow::(); - builder.allow::(); - builder.allow::(); - - // Audio components - builder.allow::>(); - builder.allow::(); - - // Text components - builder.allow::>(); - builder.allow::>(); - - level_root.iter().for_each(|level| { - // Extract the level root - builder.extract_entity(level); - - if let Ok(kids) = children.get(level) { - builder.extract_entities(kids.into_iter().map(|&e| e)); - } else { - warn!("Level is empty!"); - } - }); - - audio_root.iter().for_each(|audio| { - // Extract the level root - builder.extract_entity(audio); - - if let Ok(kids) = children.get(audio) { - builder.extract_entities(kids.into_iter().map(|&e| e)); - } else { - warn!("Audio is empty!"); - } - }); - - let scene = builder.build(); - - let serialized = scene - .serialize_ron(&app_type_registry) - .expect("Serialize scene"); - - IoTaskPool::get() - .spawn(async move { - // Write the scene RON data to file - std::fs::write(format!("assets/output.scn.ron"), serialized.as_bytes()) - .expect("Error while writing scene to file"); - }) - .detach(); - } - - pub fn rehydrate_level( - events: Query, Without)>, - mut commands: Commands, - ) { - events.iter().for_each(|entity| { - commands.entity(entity).insert(WO::default()); - }); - } -} - -use quit::*; -mod quit { - use super::*; - - use bevy::app::AppExit; - - #[derive(Debug, Component, Default)] - pub struct QuitAction; - - pub fn quit(mut exit: EventWriter) { - exit.send(AppExit); - } -} - -use asset_sync::*; -mod asset_sync { - use super::*; - - // This sets buttons to active when their associated handle is spawned - pub fn sync_asset_buttons( - events: Query<&Handle, Added>>, - buttons: Query<(Entity, &ui::TargetAsset)>, - mut commands: Commands, - ) { - events.iter().for_each(|this_handle| { - info!("Syncing {:?}", this_handle); - buttons - .iter() - .find_map(|(entity, ui::TargetAsset { handle })| { - if handle == this_handle { - Some(entity) - } else { - None - } - }) - .iter() - .for_each(|&entity| { - commands.entity(entity).insert(ui::Active); - }); - }); - } - - // Remove active when handle is despawned? - // ONLY IF there are no instances of that handle [!any(*)] - pub fn sync_remove_asset_buttons( - mut events: RemovedComponents>, - asset_entities: Query<&Handle>, - buttons: Query<(Entity, &ui::TargetAsset)>, - mut commands: Commands, - ) { - events - .iter() - .find_map(|this_asset_entity| asset_entities.get(this_asset_entity).ok()) - .iter() - .for_each(|this_handle| { - info!("Syncing removal of {:?}", this_handle); - buttons - .iter() - .find_map(|(entity, ui::TargetAsset { handle })| { - if handle == *this_handle { - Some(entity) - } else { - None - } - }) - .iter() - .for_each(|&entity| { - commands.entity(entity).remove::(); - }); - }); - } -} - -use timeline::*; -mod timeline { - use super::*; - - /// Timeline widget marker - #[derive(Debug, Component)] - pub struct TimelineWidget; - - /// Add Epoch component, used on a button to trigger a new epoch addition - #[derive(Debug, Component)] - pub struct AddEpoch; - - /// Epoch ID Component - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochId { - id: usize, - } - - /// Epoch GLTF Component - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochGltf { - gltf: Handle, - } - - /// Epoch Scene Component - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochScene { - scene: Handle, - } - - /// Epoch Camera Component, marking the current camera - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochCamera { - camera: Entity, - } - - /// Epoch music component, marking the opening track for this epoch - #[derive(Debug, Reflect, Default, Component, Clone)] - pub struct EpochMusic { - music: Handle, - } - - /// Epoch monologue, marking the dialog spoken this epoch - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochMonologue { - monologue: Handle, - } - - /// Epoch font, marking the font used for this epoch's monologue - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochFont { - font: Handle, - } - - /// A vector of audios looping this epoch as background tracks - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochSfx { - sfx: Vec>, - } - - /// Epoch animations, looping this epoch - #[derive(Debug, Reflect, Component, Clone)] - pub struct EpochAnimations { - animations: Vec>, - } - - /// System for adding an epoch to the level's timeline - /// Triggered when a button with the AddEpoch marker is Active - pub fn add_timeline_epoch( - root: Query<(Entity, &Children), With>, - mut commands: Commands, - ) { - info!("Adding timeline epoch"); - - root.iter().for_each(|(entity, children)| { - let id = children.iter().len(); - let name = format!("{}", id); - commands.entity(entity).with_children(|parent| { - parent.spawn(( - ButtonBundle { - style: Style { - border: UiRect::all(Val::Px(1.0)), - margin: UiRect::all(Val::Px(1.0)), - padding: UiRect::all(Val::Px(1.0)), - ..default() - }, - border_color: Color::BLACK.into(), - ..default() - }, - ui::Title { - text: name, - ..default() - }, - EpochId { id }, - )); - }); - }); - } - - /// Set the GLTF for the current epoch - pub fn set_epoch_gltf( - events: Query<&ui::TargetAsset, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a GLTF is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter().for_each(|entity| { - // Set the GLTF (overwrite existing GLTF selections) - commands.entity(entity).insert(EpochGltf { - gltf: handle.clone(), - }); - - // TODO: Unset Scene, Camera, Animations - }); - }); - } - - pub fn load_epoch_gltf(events: Query, (Added, With)>) { - events.iter().for_each(|epoch_gltf| { - warn!("TODO: Load epoch GLTF!"); - }) - } - - pub fn set_epoch_scene( - events: Query<&ui::TargetAsset, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter().for_each(|entity| { - // Set the Scene (overwrite existing Scene selections) - commands.entity(entity).insert(EpochScene { - scene: handle.clone(), - }); - }); - }); - } - - pub fn load_epoch_scene( - events: Query, (Added, With)>, - ) { - events.iter().for_each(|epoch_scene| { - warn!("TODO: Load epoch Scene!"); - }) - } - - pub fn set_epoch_camera( - events: Query<&ui::TargetEntity, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetEntity { entity }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter().for_each(|this_entity| { - // Set the Scene (overwrite existing Scene selections) - commands - .entity(this_entity) - .insert(EpochCamera { camera: *entity }); - }); - }); - } - - pub fn load_epoch_camera( - events: Query, (Added, With)>, - ) { - events.iter().for_each(|epoch_camera| { - warn!("TODO: Load epoch Camera"); - }) - } - - pub fn set_epoch_music( - events: Query<&ui::TargetAsset, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - info!("TODO: Select scene music"); - - // // Iterate over all (0 or 1) active epochs - // active_epoch.iter().for_each(|entity| { - // // Set the Scene (overwrite existing Scene selections) - // commands.entity(entity).insert(EpochMusic { music: handle.clone() }); - // }); - }); - } - - pub fn load_epoch_music( - events: Query, (Added, With)>, - ) { - events.iter().for_each(|epoch_music| { - warn!("TODO: Load epoch music!"); - }) - } - - pub fn set_epoch_monologue( - events: Query<&ui::TargetAsset, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter().for_each(|entity| { - // Set the Scene (overwrite existing Scene selections) - commands.entity(entity).insert(EpochMonologue { - monologue: handle.clone(), - }); - }); - }); - } - - pub fn load_epoch_monologue( - events: Query, (Added, With)>, - ) { - events.iter().for_each(|epoch_monologue| { - warn!("TODO: unset epoch Monologue!"); - epoch_monologue - .iter() - .for_each(|EpochMonologue { monologue }| { - warn!("TODO: Set level epoch"); - }); - }); - } - - pub fn set_epoch_font( - events: Query<&ui::TargetAsset, Added>, - active_epoch: Query, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter().for_each(|entity| { - // Set the Scene (overwrite existing Scene selections) - commands.entity(entity).insert(EpochFont { - font: handle.clone(), - }); - }); - }); - } - - pub fn load_epoch_font( - events: Query, (Added, With)>, - mut font_info: ResMut, - ) { - events.iter().for_each(|epoch_font| { - font_info.default = epoch_font.map(|EpochFont { font }| font.clone()); - }); - } - - pub fn set_epoch_sfx( - events: Query<&ui::TargetAsset, Added>, - mut active_epoch: Query<(Entity, Option<&mut EpochSfx>), (With, With)>, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch.iter_mut().for_each(|(entity, maybe_sfx)| { - info!("Adding sfx {:?} to epoch {:?}", maybe_sfx, entity); - if let Some(mut epoch_sfx) = maybe_sfx { - epoch_sfx.sfx.push(handle.clone()); - } else { - // Set the Scene (overwrite existing Scene selections) - commands.entity(entity).insert(EpochSfx { - sfx: vec![handle.clone()], - }); - } - }); - }); - } - - pub fn load_epoch_sfx( - added: Query, With)>, - mut removed: RemovedComponents, - epoch_sfx: Query<&EpochSfx>, - mut writer: EventWriter, - ) { - removed.iter().for_each(|entity| { - epoch_sfx.get(entity).iter().for_each(|EpochSfx { sfx }| { - sfx.iter().for_each(|handle| { - writer.send(ControlAudio::Stop(handle.clone())); - }); - }); - }); - added.iter().for_each(|entity| { - epoch_sfx.get(entity).iter().for_each(|EpochSfx { sfx }| { - sfx.iter().for_each(|handle| { - writer.send(ControlAudio::Loop(handle.clone())); - }); - }); - }); - } - - pub fn set_epoch_animations( - events: Query<&ui::TargetAsset, Added>, - mut active_epoch: Query< - (Entity, Option<&mut EpochAnimations>), - (With, With), - >, - mut commands: Commands, - ) { - // Each time a Scene is selected in the editor - events.iter().for_each(|ui::TargetAsset { handle }| { - // Iterate over all (0 or 1) active epochs - active_epoch - .iter_mut() - .for_each(|(entity, maybe_animations)| { - if let Some(mut epoch_animations) = maybe_animations { - epoch_animations.animations.push(handle.clone()); - } else { - // Set the Scene (overwrite existing Scene selections) - commands.entity(entity).insert(EpochAnimations { - animations: vec![handle.clone()], - }); - } - }); - }); - } - - pub fn load_epoch_animations( - events: Query, (Added, With)>, - ) { - events.iter().for_each(|epoch_animations| { - warn!("TODO: Load epoch Animations!"); - }) - } -} diff --git a/src/editor/animation.rs b/src/editor/animation.rs new file mode 100644 index 0000000..7fd7ff5 --- /dev/null +++ b/src/editor/animation.rs @@ -0,0 +1,155 @@ +use crate::editor::prelude::*; + +#[derive(Debug, Component, Default)] +pub struct AnimationWidget; + +#[derive(Debug, Component)] +pub struct AnimationPlayAll; + +pub fn init_animations_ui(events: Query>, mut commands: Commands) { + events.iter().for_each(|entity| { + commands.entity(entity).with_children(|parent| { + parent.spawn(( + AnimationPlayAll, + ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(1.0)), + padding: UiRect::all(Val::Px(1.0)), + ..default() + }, + border_color: Color::BLACK.into(), + ..default() + }, + ui::Title { + text: "Play All".into(), + ..default() + }, + )); + }); + }) +} + +/// When a new scene is loaded, add any newly compatible animations +/// TODO: Add target entity(s) too +pub fn add_animations_ui( + player_spawned: Query<&Name, Added>, + widget: Query>, + mut commands: Commands, + gltfs: Res>, + clips: Res>, +) { + player_spawned.iter().for_each(|player_name| { + gltfs + .iter() + .flat_map(|(_, gltf)| gltf.named_animations.iter()) + .filter_map(|(clip_name, handle)| { + clips.get(&handle).map(|clip| (clip_name, handle, clip)) + }) + .filter(|(_, _, clip)| clip.compatible_with(player_name)) + .for_each(|(clip_name, handle, _)| { + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + clip_name.clone(), + None, + ); + }); + }); +} + +// When a scene is de-selected, remove any outdated animation options +pub fn remove_animations_ui( + mut removed_players: RemovedComponents>, + current: Query<(Entity, &ui::TargetAsset)>, + clips: Res>, + targets: Query<(&AnimationPlayer, &Name)>, + mut commands: Commands, +) { + // For each removed scene + removed_players.iter().for_each(|_| { + // Iterate over the current animation buttons + current + .iter() + .filter(|(_, ui::TargetAsset { handle })| { + // Check if this clip is compatible with any remaining entities + // NOTE: We are checking this is *not* compatible with any entities + clips + .get(handle) + .map(|clip| !(targets.iter().any(|(_, name)| clip.compatible_with(name)))) + .unwrap_or(true) + }) + .for_each(|(_, ui::TargetAsset { handle })| { + // Destroy the buton if it is so + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + }); + }); +} + +pub fn play_all_animations( + start: Query, Added)>, + mut stop: RemovedComponents, + play_all_btn: Query>, + clip_btns: Query>>, + mut commands: Commands, +) { + stop.iter() + .filter(|&entity| play_all_btn.contains(entity)) + .for_each(|_| { + clip_btns.iter().for_each(|entity| { + commands.entity(entity).remove::(); + }) + }); + start + .iter() + .filter(|&entity| play_all_btn.contains(entity)) + .for_each(|_| { + clip_btns.iter().for_each(|entity| { + commands.entity(entity).insert(ui::Active); + }) + }); +} + +pub fn play_animation( + start: Query, Added)>, + mut stop: RemovedComponents, + clip_refs: Query<&ui::TargetAsset>, + mut targets: Query<(&mut AnimationPlayer, &Name), With>, + clips: Res>, +) { + stop.iter().for_each(|entity| { + if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) { + let clip = clips.get(&handle).expect("Load animation clip"); + targets + .iter_mut() + .filter(|(_, name)| clip.compatible_with(name)) + .for_each(|(mut player, _)| { + player.pause(); + }) + } + }); + start.iter().for_each(|entity| { + if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) { + let clip = clips.get(&handle).expect("Load animation clip"); + targets + .iter_mut() + .filter(|(_, name)| clip.compatible_with(name)) + .for_each(|(mut player, _)| { + if player.is_paused() { + player.resume(); + } else { + player.play(handle.clone()).repeat(); + } + }) + } + }); +} diff --git a/src/editor/asset_sync.rs b/src/editor/asset_sync.rs new file mode 100644 index 0000000..2483d19 --- /dev/null +++ b/src/editor/asset_sync.rs @@ -0,0 +1,55 @@ +use crate::editor::prelude::*; + +// This sets buttons to active when their associated handle is spawned +pub fn sync_asset_buttons( + events: Query<&Handle, Added>>, + buttons: Query<(Entity, &ui::TargetAsset)>, + mut commands: Commands, +) { + events.iter().for_each(|this_handle| { + info!("Syncing {:?}", this_handle); + buttons + .iter() + .find_map(|(entity, ui::TargetAsset { handle })| { + if handle == this_handle { + Some(entity) + } else { + None + } + }) + .iter() + .for_each(|&entity| { + commands.entity(entity).insert(ui::Active); + }); + }); +} + +// Remove active when handle is despawned? +// ONLY IF there are no instances of that handle [!any(*)] +pub fn sync_remove_asset_buttons( + mut events: RemovedComponents>, + asset_entities: Query<&Handle>, + buttons: Query<(Entity, &ui::TargetAsset)>, + mut commands: Commands, +) { + events + .iter() + .find_map(|this_asset_entity| asset_entities.get(this_asset_entity).ok()) + .iter() + .for_each(|this_handle| { + info!("Syncing removal of {:?}", this_handle); + buttons + .iter() + .find_map(|(entity, ui::TargetAsset { handle })| { + if handle == *this_handle { + Some(entity) + } else { + None + } + }) + .iter() + .for_each(|&entity| { + commands.entity(entity).remove::(); + }); + }); +} diff --git a/src/editor/assets.rs b/src/editor/assets.rs index cdb86f4..52b0cdf 100644 --- a/src/editor/assets.rs +++ b/src/editor/assets.rs @@ -1,5 +1,4 @@ -use crate::ui; -use bevy::{asset::Asset, prelude::*}; +use crate::editor::prelude::*; #[derive(Resource, Default)] pub struct AssetRegistry(pub Vec); diff --git a/src/editor/audio.rs b/src/editor/audio.rs index 9496ced..3878490 100644 --- a/src/editor/audio.rs +++ b/src/editor/audio.rs @@ -1,5 +1,5 @@ -use crate::{editor::assets::*, ui}; -use bevy::{audio::PlaybackMode, prelude::*}; +use crate::editor::prelude::*; +use bevy::audio::PlaybackMode; #[derive(Debug, Component, Reflect, Default)] #[reflect(Component)] diff --git a/src/editor/camera.rs b/src/editor/camera.rs new file mode 100644 index 0000000..f19ce19 --- /dev/null +++ b/src/editor/camera.rs @@ -0,0 +1,66 @@ +use crate::editor::prelude::*; + +#[derive(Debug, Component)] +pub struct EditorCamera; + +#[derive(Debug, Component, Default)] +pub struct CameraWidget; + +pub fn cameras_ui( + mut added: Query<(Entity, &mut Camera, &Name), Added>, + mut removed: RemovedComponents, + editor_camera: Query>, + widget: Query>, + current: Query<(Entity, &ui::TargetEntity)>, + mut commands: Commands, +) { + removed.iter().for_each(|entity| { + info!("Destroy button for {:?}", entity); + destroy_entity_button(¤t, &mut commands, &ui::TargetEntity { entity }); + }); + added.iter_mut().for_each(|(entity, mut camera, name)| { + info!("Camera added {:?} {:?}", entity, name); + create_entity_button( + &widget, + &mut commands, + ui::TargetEntity { entity }, + name.as_str().into(), + ); + camera.is_active = entity == editor_camera.single(); + }); +} + +/// Set the camera active component based on button clicks +pub fn manage_active_camera( + events: Query<&ui::TargetEntity, Added>, + mut cameras: Query<(Entity, &mut Camera)>, +) { + events.iter().for_each(|ui::TargetEntity { entity }| { + cameras.iter_mut().for_each(|(this_entity, mut camera)| { + if this_entity == *entity { + info!("Marking {:?} as active camera", entity); + camera.is_active = true; + } else { + info!("Marking {:?} as inactive camera", entity); + camera.is_active = false; + } + }); + }); +} + +// In the event that an active camera is despawned, fall back to the editor camera +pub fn fallback_camera( + modified: Query, Without)>, + mut removed: RemovedComponents, + other_cameras: Query<&Camera, Without>, + mut editor_camera: Query<&mut Camera, With>, +) { + // Any time a camera is modified + modified.iter().chain(removed.iter()).for_each(|_| { + // If no other cameras are active + if !other_cameras.iter().any(|camera| camera.is_active) { + // Make the editor camera active + editor_camera.single_mut().is_active = true; + } + }) +} diff --git a/src/editor/font.rs b/src/editor/font.rs index 38ca43f..18699de 100644 --- a/src/editor/font.rs +++ b/src/editor/font.rs @@ -1,5 +1,4 @@ -use crate::{editor::assets::*, ui}; -use bevy::prelude::*; +use crate::editor::prelude::*; #[derive(Debug, Component, Default)] pub struct FontWidget; diff --git a/src/editor/gltf.rs b/src/editor/gltf.rs new file mode 100644 index 0000000..d79317d --- /dev/null +++ b/src/editor/gltf.rs @@ -0,0 +1,68 @@ +use crate::editor::prelude::*; + +#[derive(Debug, Component, Default)] +pub struct GltfWidget; + +pub fn gltf_ui( + mut events: EventReader>, + mut commands: Commands, + widget: Query>, + current: Query<(Entity, &ui::TargetAsset)>, + server: Res, +) { + events.iter().for_each(|event| match event { + AssetEvent::Created { handle } => { + info!("Asset created! {:?}", event); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + None, + ); + } + AssetEvent::Removed { handle } => { + info!("Asset removed! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + } + AssetEvent::Modified { handle } => { + info!("Asset modified! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + None, + ); + } + }); +} + +pub fn control_active_gltf( + events: Query>, Added)>, + root: Query>, + mut commands: Commands, +) { + events.iter().for_each(|_| { + root.iter().for_each(|entity| { + commands.entity(entity).despawn_descendants(); + }); + }); +} diff --git a/src/editor/level.rs b/src/editor/level.rs new file mode 100644 index 0000000..f9dbebb --- /dev/null +++ b/src/editor/level.rs @@ -0,0 +1,167 @@ +use crate::editor::prelude::*; +use bevy::tasks::IoTaskPool; + +#[derive(Debug, Component, Reflect, Default)] +#[reflect(Component)] +pub struct LevelRoot; + +pub type Level = DynamicScene; + +#[derive(Debug, Component, Default)] +pub struct LevelWidget; + +#[derive(Debug, Component)] +pub struct ExportLevel; + +pub fn level_ui( + mut events: EventReader>, + mut commands: Commands, + widget: Query>, + current: Query<(Entity, &ui::TargetAsset)>, + server: Res, +) { + events.iter().for_each(|event| match event { + AssetEvent::Created { handle } => { + info!("Asset created! {:?}", event); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + None, + ); + } + AssetEvent::Removed { handle } => { + info!("Asset removed! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + } + AssetEvent::Modified { handle } => { + info!("Asset modified! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + None, + ); + } + }); +} + +pub fn load_level( + events: Query< + &ui::TargetAsset, + (Added, With>), + >, + root: Query>, + mut commands: Commands, +) { + events.iter().for_each(|ui::TargetAsset { handle }| { + root.iter().for_each(|entity| { + commands.entity(entity).despawn_recursive(); + }); + commands.spawn(DynamicSceneBundle { + scene: handle.clone(), + ..default() + }); + }); +} + +pub fn export_level( + level_root: Query>, + audio_root: Query>, + children: Query<&Children>, + world: &World, +) { + let app_type_registry = world.resource::().clone(); + let mut builder = DynamicSceneBuilder::from_world(world.clone()); + + builder.deny_all_resources(); + + // Exclude computed visibility + builder.deny_all(); + + // Level administrivia + builder.allow::(); + builder.allow::(); + + // TODO: Serialize Timeline + + // Scene components + builder.allow::>(); + + // Spatial components + builder.allow::(); + builder.allow::(); + builder.allow::(); + + // Audio components + builder.allow::>(); + builder.allow::(); + + // Text components + builder.allow::>(); + builder.allow::>(); + + level_root.iter().for_each(|level| { + // Extract the level root + builder.extract_entity(level); + + if let Ok(kids) = children.get(level) { + builder.extract_entities(kids.into_iter().map(|&e| e)); + } else { + warn!("Level is empty!"); + } + }); + + audio_root.iter().for_each(|audio| { + // Extract the level root + builder.extract_entity(audio); + + if let Ok(kids) = children.get(audio) { + builder.extract_entities(kids.into_iter().map(|&e| e)); + } else { + warn!("Audio is empty!"); + } + }); + + let scene = builder.build(); + + let serialized = scene + .serialize_ron(&app_type_registry) + .expect("Serialize scene"); + + IoTaskPool::get() + .spawn(async move { + // Write the scene RON data to file + std::fs::write(format!("assets/output.scn.ron"), serialized.as_bytes()) + .expect("Error while writing scene to file"); + }) + .detach(); +} + +pub fn rehydrate_level( + events: Query, Without)>, + mut commands: Commands, +) { + events.iter().for_each(|entity| { + commands.entity(entity).insert(WO::default()); + }); +} diff --git a/src/editor/lighting.rs b/src/editor/lighting.rs new file mode 100644 index 0000000..1a18283 --- /dev/null +++ b/src/editor/lighting.rs @@ -0,0 +1,21 @@ +use crate::editor::prelude::*; + +pub fn spot_light_force_shadows(mut spot_lights: Query<&mut SpotLight, Added>) { + spot_lights.iter_mut().for_each(|mut light| { + light.shadows_enabled = true; + }) +} + +pub fn directional_light_force_shadows( + mut directional_lights: Query<&mut DirectionalLight, Added>, +) { + directional_lights.iter_mut().for_each(|mut light| { + light.shadows_enabled = true; + }) +} + +pub fn point_light_force_shadows(mut point_lights: Query<&mut PointLight, Added>) { + point_lights.iter_mut().for_each(|mut light| { + light.shadows_enabled = true; + }) +} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 4ae372b..1fc0b3c 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,10 +1,18 @@ -pub mod audio; - +pub mod animation; +pub mod asset_sync; pub mod assets; - -pub mod monologue; - +pub mod audio; +pub mod camera; pub mod font; +pub mod gltf; +pub mod level; +pub mod lighting; +pub mod monologue; +mod prelude; +pub mod quit; +pub mod reset; +pub mod scene; +pub mod timeline; use crate::ui; use bevy::{asset::Asset, prelude::*}; diff --git a/src/editor/monologue.rs b/src/editor/monologue.rs index f3ae7b4..f73e591 100644 --- a/src/editor/monologue.rs +++ b/src/editor/monologue.rs @@ -1,10 +1,6 @@ -use crate::{ - editor::{assets::*, font::*}, - ui, -}; +use crate::editor::prelude::*; use bevy::{ asset::{AssetLoader, LoadContext, LoadedAsset}, - prelude::*, reflect::{TypePath, TypeUuid}, ui::FocusPolicy, utils::BoxedFuture, diff --git a/src/editor/prelude.rs b/src/editor/prelude.rs new file mode 100644 index 0000000..7b1604f --- /dev/null +++ b/src/editor/prelude.rs @@ -0,0 +1,8 @@ +pub use crate::{ + editor::{ + animation::*, asset_sync::*, assets::*, audio::*, camera::*, font::*, gltf::*, level::*, + lighting::*, monologue::*, quit::*, reset::*, scene::*, timeline::*, *, + }, + ui, +}; +pub use bevy::{gltf::Gltf, prelude::*}; diff --git a/src/editor/quit.rs b/src/editor/quit.rs new file mode 100644 index 0000000..c3c0b87 --- /dev/null +++ b/src/editor/quit.rs @@ -0,0 +1,10 @@ +use crate::editor::prelude::*; + +use bevy::app::AppExit; + +#[derive(Debug, Component, Default)] +pub struct QuitAction; + +pub fn quit(mut exit: EventWriter) { + exit.send(AppExit); +} diff --git a/src/editor/reset.rs b/src/editor/reset.rs new file mode 100644 index 0000000..2408c83 --- /dev/null +++ b/src/editor/reset.rs @@ -0,0 +1,69 @@ +use crate::editor::prelude::*; + +#[derive(Debug, Component)] +pub struct ClearLevel; + +pub fn clear_level( + events: Query, Added)>, + actives: Query< + Entity, + ( + With, + Or<( + With, + With>, + With>, + With>, + With>, + With>, + With>, + )>, + ), + >, + root: Query>, + mut commands: Commands, +) { + events.iter().for_each(|_| { + actives.iter().for_each(|entity| { + commands.entity(entity).remove::(); + }); + root.iter().for_each(|entity| { + commands.entity(entity).despawn_descendants(); + }); + }) +} + +#[derive(Debug, Component)] +pub struct ClearAssets; + +pub fn clear_assets( + asset_holders: Query< + Entity, + Or<( + With>, + With>, + With>, + With>, + With>, + With>, + With>, + With>, + With>, + With>, + With>, + With>, + )>, + >, + mut registry: ResMut, + mut commands: Commands, +) { + info!("Clearing assets"); + + // Clear buttons holding asset references + asset_holders + .iter() + .for_each(|entity| commands.entity(entity).despawn_recursive()); + + // Empty asset registry + registry.0.clear(); +} diff --git a/src/editor/scene.rs b/src/editor/scene.rs new file mode 100644 index 0000000..7eddf59 --- /dev/null +++ b/src/editor/scene.rs @@ -0,0 +1,93 @@ +use crate::editor::prelude::*; + +#[derive(Debug, Component, Default)] +pub struct SceneWidget; + +pub fn add_scenes_ui( + gltf_selected: Query<&ui::TargetAsset, Added>, + mut commands: Commands, + gltfs: Res>, + widget: Query>, +) { + gltf_selected.iter().for_each(|ui::TargetAsset { handle }| { + if let Some(gltf) = gltfs.get(&handle.clone()) { + gltf.named_scenes.iter().for_each(|(name, handle)| { + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + name.clone(), + None, + ); + }) + } + }); +} + +pub fn remove_scenes_ui( + mut gltf_unselected: RemovedComponents, + target_assets: Query<&ui::TargetAsset>, + current: Query<(Entity, &ui::TargetAsset)>, + gltfs: Res>, + mut commands: Commands, +) { + gltf_unselected + .iter() + .filter_map(|entity| target_assets.get(entity).ok()) + .filter_map(|ui::TargetAsset { handle }| gltfs.get(handle)) + .for_each(|gltf| { + gltf.scenes.iter().for_each(|handle| { + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + }); + }); +} + +pub fn control_active_scenes( + added: Query, Added)>, + mut removed: RemovedComponents, + scene_refs: Query<&ui::TargetAsset>, + scenes: Query<(Entity, &Handle)>, + level_root: Query>, + mut commands: Commands, +) { + // A scene button was marked inactive + removed.iter().for_each(|entity| { + // Get the handle associated with that button + scene_refs + .get(entity) + .iter() + .for_each(|ui::TargetAsset { handle }| { + scenes + .iter() + .find_map(|(entity, this_handle)| (this_handle == handle).then_some(entity)) + .iter() + .for_each(|&entity| { + commands.entity(entity).despawn_recursive(); + }); + }); + }); + added.iter().for_each(|entity| { + scene_refs + .get(entity) + .iter() + .for_each(|ui::TargetAsset { handle }| { + info!("Spawning Scene {:?}", handle); + commands + .entity(level_root.single()) + .with_children(|parent| { + parent.spawn(SceneBundle { + scene: handle.clone(), + ..default() + }); + }); + }); + }); +} diff --git a/src/editor/timeline.rs b/src/editor/timeline.rs new file mode 100644 index 0000000..cb70ea6 --- /dev/null +++ b/src/editor/timeline.rs @@ -0,0 +1,325 @@ +use crate::editor::prelude::*; + +/// Timeline widget marker +#[derive(Debug, Component)] +pub struct TimelineWidget; + +/// Add Epoch component, used on a button to trigger a new epoch addition +#[derive(Debug, Component)] +pub struct AddEpoch; + +/// Epoch ID Component +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochId { + id: usize, +} + +/// Epoch GLTF Component +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochGltf { + gltf: Handle, +} + +/// Epoch Scene Component +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochScene { + scene: Handle, +} + +/// Epoch Camera Component, marking the current camera +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochCamera { + camera: Entity, +} + +/// Epoch music component, marking the opening track for this epoch +#[derive(Debug, Reflect, Default, Component, Clone)] +pub struct EpochMusic { + music: Handle, +} + +/// Epoch monologue, marking the dialog spoken this epoch +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochMonologue { + monologue: Handle, +} + +/// Epoch font, marking the font used for this epoch's monologue +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochFont { + font: Handle, +} + +/// A vector of audios looping this epoch as background tracks +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochSfx { + sfx: Vec>, +} + +/// Epoch animations, looping this epoch +#[derive(Debug, Reflect, Component, Clone)] +pub struct EpochAnimations { + animations: Vec>, +} + +/// System for adding an epoch to the level's timeline +/// Triggered when a button with the AddEpoch marker is Active +pub fn add_timeline_epoch( + root: Query<(Entity, &Children), With>, + mut commands: Commands, +) { + info!("Adding timeline epoch"); + + root.iter().for_each(|(entity, children)| { + let id = children.iter().len(); + let name = format!("{}", id); + commands.entity(entity).with_children(|parent| { + parent.spawn(( + ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(1.0)), + padding: UiRect::all(Val::Px(1.0)), + ..default() + }, + border_color: Color::BLACK.into(), + ..default() + }, + ui::Title { + text: name, + ..default() + }, + EpochId { id }, + )); + }); + }); +} + +/// Set the GLTF for the current epoch +pub fn set_epoch_gltf( + events: Query<&ui::TargetAsset, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a GLTF is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter().for_each(|entity| { + // Set the GLTF (overwrite existing GLTF selections) + commands.entity(entity).insert(EpochGltf { + gltf: handle.clone(), + }); + + // TODO: Unset Scene, Camera, Animations + }); + }); +} + +pub fn load_epoch_gltf(events: Query, (Added, With)>) { + events.iter().for_each(|epoch_gltf| { + warn!("TODO: Load epoch GLTF!"); + }) +} + +pub fn set_epoch_scene( + events: Query<&ui::TargetAsset, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter().for_each(|entity| { + // Set the Scene (overwrite existing Scene selections) + commands.entity(entity).insert(EpochScene { + scene: handle.clone(), + }); + }); + }); +} + +pub fn load_epoch_scene(events: Query, (Added, With)>) { + events.iter().for_each(|epoch_scene| { + warn!("TODO: Load epoch Scene!"); + }) +} + +pub fn set_epoch_camera( + events: Query<&ui::TargetEntity, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetEntity { entity }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter().for_each(|this_entity| { + // Set the Scene (overwrite existing Scene selections) + commands + .entity(this_entity) + .insert(EpochCamera { camera: *entity }); + }); + }); +} + +pub fn load_epoch_camera(events: Query, (Added, With)>) { + events.iter().for_each(|epoch_camera| { + warn!("TODO: Load epoch Camera"); + }) +} + +pub fn set_epoch_music( + events: Query<&ui::TargetAsset, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + info!("TODO: Select scene music"); + + // // Iterate over all (0 or 1) active epochs + // active_epoch.iter().for_each(|entity| { + // // Set the Scene (overwrite existing Scene selections) + // commands.entity(entity).insert(EpochMusic { music: handle.clone() }); + // }); + }); +} + +pub fn load_epoch_music(events: Query, (Added, With)>) { + events.iter().for_each(|epoch_music| { + warn!("TODO: Load epoch music!"); + }) +} + +pub fn set_epoch_monologue( + events: Query<&ui::TargetAsset, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter().for_each(|entity| { + // Set the Scene (overwrite existing Scene selections) + commands.entity(entity).insert(EpochMonologue { + monologue: handle.clone(), + }); + }); + }); +} + +pub fn load_epoch_monologue( + events: Query, (Added, With)>, +) { + events.iter().for_each(|epoch_monologue| { + warn!("TODO: unset epoch Monologue!"); + epoch_monologue + .iter() + .for_each(|EpochMonologue { monologue }| { + warn!("TODO: Set level epoch"); + }); + }); +} + +pub fn set_epoch_font( + events: Query<&ui::TargetAsset, Added>, + active_epoch: Query, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter().for_each(|entity| { + // Set the Scene (overwrite existing Scene selections) + commands.entity(entity).insert(EpochFont { + font: handle.clone(), + }); + }); + }); +} + +pub fn load_epoch_font( + events: Query, (Added, With)>, + mut font_info: ResMut, +) { + events.iter().for_each(|epoch_font| { + font_info.default = epoch_font.map(|EpochFont { font }| font.clone()); + }); +} + +pub fn set_epoch_sfx( + events: Query<&ui::TargetAsset, Added>, + mut active_epoch: Query<(Entity, Option<&mut EpochSfx>), (With, With)>, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch.iter_mut().for_each(|(entity, maybe_sfx)| { + info!("Adding sfx {:?} to epoch {:?}", maybe_sfx, entity); + if let Some(mut epoch_sfx) = maybe_sfx { + epoch_sfx.sfx.push(handle.clone()); + } else { + // Set the Scene (overwrite existing Scene selections) + commands.entity(entity).insert(EpochSfx { + sfx: vec![handle.clone()], + }); + } + }); + }); +} + +pub fn load_epoch_sfx( + added: Query, With)>, + mut removed: RemovedComponents, + epoch_sfx: Query<&EpochSfx>, + mut writer: EventWriter, +) { + removed.iter().for_each(|entity| { + epoch_sfx.get(entity).iter().for_each(|EpochSfx { sfx }| { + sfx.iter().for_each(|handle| { + writer.send(ControlAudio::Stop(handle.clone())); + }); + }); + }); + added.iter().for_each(|entity| { + epoch_sfx.get(entity).iter().for_each(|EpochSfx { sfx }| { + sfx.iter().for_each(|handle| { + writer.send(ControlAudio::Loop(handle.clone())); + }); + }); + }); +} + +pub fn set_epoch_animations( + events: Query<&ui::TargetAsset, Added>, + mut active_epoch: Query< + (Entity, Option<&mut EpochAnimations>), + (With, With), + >, + mut commands: Commands, +) { + // Each time a Scene is selected in the editor + events.iter().for_each(|ui::TargetAsset { handle }| { + // Iterate over all (0 or 1) active epochs + active_epoch + .iter_mut() + .for_each(|(entity, maybe_animations)| { + if let Some(mut epoch_animations) = maybe_animations { + epoch_animations.animations.push(handle.clone()); + } else { + // Set the Scene (overwrite existing Scene selections) + commands.entity(entity).insert(EpochAnimations { + animations: vec![handle.clone()], + }); + } + }); + }); +} + +pub fn load_epoch_animations( + events: Query, (Added, With)>, +) { + events.iter().for_each(|epoch_animations| { + warn!("TODO: Load epoch Animations!"); + }) +}