// Monologue Trees Editor // // Editor for creating Monologue Trees levels // // TODO: // * Tree Organization: GLTF contains Animations and Scenes // * Camera can only select one at a time. // * (hard) Better Colorscheme // * (medium) Visual errors for bad GLTFs // * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras) // * (medium) Show/hide entire UI // * (medium) Spawn clicked scene // * (medium) Play clicked animation // * (idea) Use enum instead of markers for exclusive UI // * (medium) Add fonts similar to Audios based on inspect-fonts // * (hard) Add Dialogs (requires text box UI, saving, loading) use bevy::{ asset::{AssetPath, Assets}, gltf::Gltf, input::{keyboard::KeyboardInput, ButtonState}, prelude::*, utils::HashSet, }; use monologue_trees::{debug::*, ui::*}; fn main() { App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Monologue Trees Editor".into(), resolution: (640., 480.).into(), ..default() }), ..default() }), DebugInfoPlugin, GameUiPlugin, )) .init_resource::() .add_systems(Startup, (initialize_ui,)) .add_systems( Update, ( // GLTF Systems load_gltf, unload_gltf, manage_gltf_ui, // Scene Systems manage_scene_ui, spawn_scene, // Animation systems manage_animation_ui, // Camera systems manage_camera_ui, // Audio Systems load_audio, unload_audio, manage_audio_ui, play_audio, // Level Import/Export systems export_level, import_level, // Misc/Debug Systems load_bogus, ), ) .run(); } /// A generic referenece used for UI elements to point to assets or entities #[derive(Debug, Component)] enum UiRef { Handle(T), // Entity(T), // Event(T), } /// UI: /// * GLTFs /// * Scenes /// * Cameras /// * Animations /// * Audios fn initialize_ui(mut commands: Commands) { commands.spawn(( Camera2dBundle { ..default() }, UiCameraConfig { show_ui: true }, )); commands .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, ..default() }) .with_children(|parent| { parent .spawn(( GameUiList, Name::new("GLTFs"), NodeBundle { style: Style { flex_direction: FlexDirection::Column, ..default() }, ..default() }, GltfsUi, )) .with_children(|parent| { parent.spawn(( GameUiList, Name::new("Scenes"), NodeBundle { ..default() }, ScenesUi, )); parent.spawn(( GameUiList, Name::new("Cameras"), NodeBundle { ..default() }, CamerasUi, )); parent.spawn(( GameUiList, Name::new("Animations"), NodeBundle { ..default() }, AnimationsUi, )); }); parent.spawn(( GameUiSet, Name::new("Audio Clips"), NodeBundle { style: Style { flex_direction: FlexDirection::Column, ..default() }, ..default() }, AudioClipsUi, )); }); } fn load_bogus( mut events: EventReader, root: Query, Without>>)>, mut commands: Commands, ) { events .iter() .filter( |&KeyboardInput { key_code, state, .. }| *key_code == Some(KeyCode::Space) && *state == ButtonState::Pressed, ) .for_each(|_| { commands .spawn((GameUiButton, Name::new("bogus"), NodeBundle { ..default() })) .set_parent(root.single()); }) } #[derive(Resource, Default, Debug)] struct AssetRegistry(HashSet); /// Component marking UI for loaded Gltf assets #[derive(Component)] struct GltfsUi; /// Drag+Drop import GLTF to editor fn load_gltf( mut events: EventReader, server: Res, mut assets: ResMut, ) { events .iter() .filter_map(|event| match event { FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf), _ => None, }) .for_each(|path_buf| { let path = path_buf.as_path(); let handle = server.load_untyped(path); assets.0.insert(handle); }); } /// Helper method to extract the stripped filename given a full asset path fn get_fname(asset_path: AssetPath, suffixes: &[&str]) -> String { let path = asset_path.path().file_name().expect("Filename"); let path_str = path.to_str().expect("Asset Path to Str"); let name_str = suffixes .iter() .rfold(path_str, |acc, &suffix| acc.trim_end_matches(suffix)); String::from(name_str) } /// Sync GLTF assets with UI /// /// TODO: Handle failed load events /// Options: /// * Show Error message, do not add to UI /// * Add to UI with visual indicator /// This should be a separate async system fn manage_gltf_ui( mut events: EventReader>, root: Query, Without>>)>, mut commands: Commands, server: Res, ) { events .iter() .filter_map(|event| match event { AssetEvent::Created { handle } => { let asset_path = server .get_handle_path(handle.clone()) .expect("Fetch Asset Path"); let name = get_fname(asset_path, &[".gltf", ".glb"]); Some((handle.clone(), String::from(name))) } _ => None, }) .for_each(|(handle, name)| { root.iter().for_each(|entity| { commands.entity(entity).log_components(); }); commands .spawn(( GameUiButton, Name::new(name), NodeBundle { ..default() }, GltfsUi, UiRef::Handle(handle.clone()), )) .set_parent(root.single()); }); } /// Remove gltf from editor fn unload_gltf() {} /// Component marking UI for Scene assets #[derive(Component)] struct ScenesUi; /// Sync scene assets with UI fn manage_scene_ui( mut events: EventReader>, root: Query, Without>>)>, mut commands: Commands, gltfs: Res>, registry: Res, ) { events .iter() .filter_map(|event| match event { AssetEvent::Created { handle } => { let name = registry .0 .iter() .find_map( |gltf_handle| match gltfs.get(&gltf_handle.clone().typed::()) { Some(gltf) => { gltf.named_scenes.iter().find_map(|(name, scene_handle)| { (scene_handle == handle).then_some(name) }) } None => None, }, ) .expect("Find scene name"); Some((handle.clone(), String::from(name))) } _ => None, }) .for_each(|(handle, name)| { commands .spawn(( GameUiButton, Name::new(name), NodeBundle { ..default() }, ScenesUi, UiRef::Handle(handle.clone()), )) .set_parent(root.single()); }); } /// Component marking UI for Camera assets #[derive(Component)] struct CamerasUi; fn manage_camera_ui() {} /// Component marking UI for Animation assets #[derive(Component)] struct AnimationsUi; fn manage_animation_ui( mut events: EventReader>, root: Query, Without>>)>, mut commands: Commands, gltfs: Res>, registry: Res, ) { events .iter() .filter_map(|event| match event { AssetEvent::Created { handle } => { let name = registry .0 .iter() .find_map(|gltf_handle| { match gltfs.get(&gltf_handle.clone().typed::()) { Some(gltf) => gltf.named_animations.iter().find_map( |(name, animation_handle)| { (animation_handle == handle).then_some(name) }, ), None => None, } }) .expect("Find animation name"); Some((handle.clone(), String::from(name))) } _ => None, }) .for_each(|(handle, name)| { commands .spawn(( GameUiButton, Name::new(name), NodeBundle { ..default() }, AnimationsUi, UiRef::Handle(handle.clone()), )) .set_parent(root.single()); }); } /// Component marking UI for Audio Clip assets #[derive(Component)] struct AudioClipsUi; /// Drag+Drop import Audio to editor fn load_audio( mut events: EventReader, server: Res, mut assets: ResMut, ) { events .iter() .filter_map(|event| match event { FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf), _ => None, }) .for_each(|path_buf| { let path = path_buf.as_path(); let handle = server.load_untyped(path); assets.0.insert(handle); }); } fn manage_audio_ui( mut events: EventReader>, root: Query, Without)>, mut commands: Commands, server: Res, ) { events .iter() .filter_map(|event| match event { AssetEvent::Created { handle } => { let asset_path = server .get_handle_path(handle.clone()) .expect("Fetch Asset Path"); let name = get_fname(asset_path, &[".ogg"]); Some((handle.clone(), String::from(name))) } _ => None, }) .for_each(|(handle, name)| { commands .spawn(( GameUiButton, Name::new(name), NodeBundle { ..default() }, AudioClipsUi, AudioBundle { source: handle.clone(), settings: PlaybackSettings { mode: bevy::audio::PlaybackMode::Loop, paused: true, ..default() }, }, )) .set_parent(root.single()); }); } /// Play/Loop Audio fn play_audio( mut events: Query< (&Interaction, &AudioSink, &mut UiElementState), (Changed, With), >, ) { events .iter_mut() .for_each(|(interaction, sink, mut state)| match interaction { Interaction::Pressed => { sink.toggle(); *state = match *state { UiElementState::Enabled => UiElementState::Active, _ => UiElementState::Enabled, } } _ => (), }); } /// Remove audio from editor fn unload_audio() {} /// Spawn Scene fn spawn_scene() {} /// Export level fn export_level() {} /// Import Level fn import_level() {}