// Monologe Trees Editor // // Editor for creating Monologue Trees levels // // REFACTOR: // * Monologue -> Event -> Active -> UI // * Scene -> Event -> Active -> UI // * Animation -> Event -> Active -> UI // * Font -> Event -> Active -> UI // * Gltf -> Event -> Active -> UI // // BUGS: // * When Handle is loaded, the button for TargetAsset should load as well // * Exported level should preserve active camera // * Picking new GLTF resets audio without resetting buttons // // TODO: // * edit textbox with actions // * (brutal) export level // * (hard) import level // * (hard) Harden Active Camera // * (medium) Pre-compute animation target entities // * make min/max/close buttons into actions not selects // * (???) Better handle hide/close monologue use bevy::{ asset::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset}, gltf::Gltf, prelude::*, utils::{BoxedFuture, Duration}, }; use monologue_trees::{ debug::*, editor::{assets::*, audio::*, *}, ui, }; const WELCOME_MESSAGES: &'static [&'static str] = &[ "Welcome to the Monologue Trees editor!", "Import assets by dragging and dropping files or folders into the editor!", concat!( "Supported file types (for now):\n", "* 3D: .gltf, .glb\n", "* Audio: .ogg\n", "* Font: .ttf, .otf\n", "* Monologues: .monologue.txt", ), ]; fn main() { App::new() .add_plugins(( DefaultPlugins .set(WindowPlugin { primary_window: Some(Window { title: "Monologue Trees Editor".into(), resolution: (640., 480.).into(), ..default() }), ..default() }) .set(AssetPlugin { watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), ..default() }), DebugInfoPlugin, ui::GameUiPlugin { enable_alerts: true, }, )) .register_type::() .register_type::() .init_resource::() .init_resource::() .add_asset::() .init_asset_loader::() .add_event::() .add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message)) .add_systems(Update, quit.run_if(ui::activated::)) .add_systems( Update, ( init_animations_ui, remove_animations_ui, add_animations_ui, play_all_animations, play_animation, ), ) .add_systems( Update, (remove_scenes_ui, add_scenes_ui, control_active_scenes), ) .add_systems(Update, (cameras_ui, manage_active_camera, fallback_camera)) .add_systems( Update, ( audio_ui, ui_control_audio, ui_active::, ui_inactive::, control_audio, ), ) .add_systems( Update, ( gltf_ui, texts_ui, control_active_gltf, show_preview_text, sync_monologue_font, ), ) .add_systems(Update, (fonts_ui, set_active_font)) .add_systems(Startup, reload_assets) .add_systems( Update, ( reload_assets.run_if(ui::activated::), clear_assets.run_if(ui::activated::), ), ) .add_systems( Update, ( point_light_force_shadows, spot_light_force_shadows, directional_light_force_shadows, ), ) .add_systems(Update, clear_level) .add_systems( Update, ( level_ui, load_level, export_level.run_if(ui::activated::), rehydrate_level::, rehydrate_level::, PlaybackSettings>, //rehydrate_level::, AnimationPlayer>, //rehydrate_level::, ), ) .add_systems( Update, ( sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, ), ) .add_systems( Update, ( add_timeline_epoch.run_if(ui::activated::), set_epoch_gltf, load_epoch_gltf, set_epoch_scene, load_epoch_scene, set_epoch_camera, load_epoch_camera, set_epoch_music, load_epoch_music, set_epoch_monologue, load_epoch_monologue, set_epoch_font, load_epoch_font, set_epoch_sfx, load_epoch_sfx, set_epoch_animations, load_epoch_animations, ), ) .run(); } #[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)); commands.spawn(AudioRoot); commands.spawn(( Camera2dBundle { ..default() }, UiCameraConfig { show_ui: true }, Name::new("Editor Camera"), EditorCamera, ui::Active, )); let base_style = Style { border: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), overflow: Overflow::clip(), flex_direction: FlexDirection::Column, ..default() }; let simple_button = ButtonBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }; // Assets widget commands .spawn(NodeBundle { style: Style { top: Val::Px(0.0), left: Val::Px(0.0), position_type: PositionType::Absolute, border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }) .with_children(|parent| { let container = parent .spawn((NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Row, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() },)) .with_children(|parent| { // HACK: This is super janky but I think we need it like this for UI layout rules let mut content_containers: Vec<(String, Entity)> = Vec::new(); // Containers with asset content parent .spawn(( NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), justify_content: JustifyContent::FlexStart, ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Sorting(2), )) .with_children(|parent| { content_containers .push(spawn_tab_container::("Font", parent)); content_containers .push(spawn_tab_container::("Monologue", parent)); content_containers .push(spawn_tab_container::("Audio", parent)); content_containers .push(spawn_tab_container::("Level", parent)); content_containers .push(spawn_tab_container::("Gltf", parent)); content_containers .push(spawn_tab_container::("Scene", parent)); content_containers .push(spawn_tab_container::("Camera", parent)); content_containers .push(spawn_tab_container::("Animation", parent)); }); // Container for tabs that open/close containers parent .spawn(( NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Sorting(1), ui::Select::Single, )) .with_children(|parent| { content_containers.iter().enumerate().for_each( |(i, (name, target))| { parent.spawn(( simple_button.clone(), ui::Title { text: name.clone(), ..default() }, ui::Collapse { target: *target }, ui::Sorting(i as u8), )); }, ); }); }) .id(); parent.spawn(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Assets".into(), ..default() }, ui::Minimize { target: container }, ui::Sorting(0), )); }); // Actions widget commands .spawn(NodeBundle { style: Style { bottom: Val::Px(0.0), left: Val::Px(0.0), position_type: PositionType::Absolute, border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }) .with_children(|parent| { let container = parent .spawn(( NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Sorting(99), ui::Select::Action, )) .with_children(|parent| { parent.spawn(( simple_button.clone(), ReloadAssets, ui::Sorting(1), ui::Title { text: "Reload Assets".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ClearAssets, ui::Sorting(2), ui::Title { text: "Clear Assets".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ExportLevel, ui::Sorting(3), ui::Title { text: "Export Level".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ClearLevel, ui::Sorting(3), ui::Title { text: "Clear Level".into(), ..default() }, )); parent.spawn(( simple_button.clone(), QuitAction, ui::Sorting(3), ui::Title { text: "Quit".into(), ..default() }, )); }) .id(); parent.spawn(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Actions".into(), ..default() }, ui::Minimize { target: container }, ui::Sorting(0), )); }); // Actions widget commands .spawn(NodeBundle { style: Style { bottom: Val::Px(0.0), right: Val::Px(0.0), position_type: PositionType::Absolute, border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }) .with_children(|parent| { let container = parent .spawn(( NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Row, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Sorting(99), ui::Select::Single, TimelineWidget, )) .with_children(|parent| { // "Add Epoch" button parent .spawn(( NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Row, align_items: AlignItems::Center, justify_items: JustifyItems::Center, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Select::Action, )) .with_children(|parent| { parent.spawn(( simple_button.clone(), AddEpoch, ui::Title { text: "+".into(), ..default() }, )); }); }) .id(); parent.spawn(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Timeline".into(), ..default() }, ui::Minimize { target: container }, ui::Sorting(0), )); }); } fn welcome_message(mut writer: EventWriter) { WELCOME_MESSAGES .iter() .for_each(|&msg| writer.send(ui::Alert::Info(msg.into()))) } fn spawn_tab_container( title: &'static str, parent: &mut ChildBuilder, ) -> (String, Entity) { ( title.into(), // Content node parent .spawn(( NodeBundle { style: Style { display: Display::None, border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, T::default(), ui::Scroll, Interaction::default(), )) .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 fonts::*; mod fonts { use super::*; #[derive(Debug, Component, Default)] pub struct FontWidget; #[derive(Debug, Resource, Default)] pub struct FontInfo { pub default: Option>, } pub fn fonts_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()), Some(handle.clone()), ); } 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()), Some(handle.clone()), ); } }); } pub fn set_active_font( events: Query<&ui::TargetAsset, Added>, mut font: ResMut, ) { events .iter() .for_each(|ui::TargetAsset { handle }| font.default = Some(handle.clone())); } } use monologues::*; mod monologues { use super::*; use bevy::{ reflect::{TypePath, TypeUuid}, ui::FocusPolicy, }; use serde::Deserialize; #[derive(Debug, Component, Default)] pub struct MonologueWidget; #[derive(Debug, Deserialize, TypeUuid, TypePath, PartialEq)] #[uuid = "216a570b-d142-4026-baed-d7feb0250458"] pub struct Monologue { text: String, } #[derive(Debug, Component)] pub struct MonologueModal; #[derive(Debug, Component)] pub struct MonologueContainer; #[derive(Default)] pub struct MonologueLoader; impl AssetLoader for MonologueLoader { fn load<'a>( &'a self, bytes: &'a [u8], load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<(), bevy::asset::Error>> { Box::pin(async move { let asset = Monologue { text: String::from_utf8(bytes.to_vec())?, }; load_context.set_default_asset(LoadedAsset::new(asset)); Ok(()) }) } fn extensions(&self) -> &[&str] { &["monologue.txt"] } } pub fn init_texts_ui(mut commands: Commands) { commands.spawn(( NodeBundle { style: Style { width: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }, focus_policy: FocusPolicy::Pass, ..default() }, MonologueContainer, )); } pub fn texts_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!("Monologue created! {:?}", event); create_asset_button( &widget, &mut commands, ui::TargetAsset { handle: handle.clone(), }, get_asset_name(&server, handle.clone()), None, ); } AssetEvent::Removed { handle } => { info!("Monologue removed! {:?}", event); destroy_asset_button( ¤t, &mut commands, &ui::TargetAsset { handle: handle.clone(), }, ); } AssetEvent::Modified { handle } => { info!("Monologue 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 show_preview_text( added: Query, Added)>, monologue_handles: Query<&ui::TargetAsset>, monologues: Res>, container: Query>, mut commands: Commands, font: Res, ) { added .iter() .filter_map(|entity| monologue_handles.get(entity).ok()) .for_each(|ui::TargetAsset { handle }| { let monologue = monologues.get(handle).expect("Preview loaded monologue"); commands .entity(container.single()) .despawn_descendants() .with_children(|parent| { parent .spawn(NodeBundle { style: Style { max_width: Val::Percent(50.0), padding: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), border: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }) .with_children(|parent| { parent.spawn(( ui::TitleBarBase::new(Color::VIOLET).bundle(), ui::Title { text: "Monologue".into(), ..default() }, ui::Close { target: parent.parent_entity(), }, ui::Sorting(0), )); let style = match &font.default { Some(handle) => TextStyle { color: Color::BLACK.into(), font_size: 16.0, font: handle.clone(), ..default() }, None => TextStyle { color: Color::BLACK.into(), font_size: 16.0, ..default() }, }; parent.spawn(( TextBundle::from_section(monologue.text.clone(), style), handle.clone(), )); }); }); }); } // TODO: Sync Handle and TextStyle components to automagically generate and sync text pub fn sync_monologue_font( mut texts: Query<&mut Text, With>>, font: Res, ) { if font.is_changed() || font.is_added() { texts.iter_mut().for_each(|mut text| { text.sections .iter_mut() .for_each(|section| match &font.default { Some(handle) => section.style.font = handle.clone(), None => section.style.font = Handle::default(), }); }); } } } 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!"); }) } }