// 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, ui::GameUiPlugin, )) .init_resource::() .add_systems(Startup, initialize_ui) .add_systems(Update, (import_files, import_audio, play_audio)) .run(); } #[derive(Resource, Default)] struct AssetRegistry(Vec); #[derive(Resource)] struct Styles { button: Style, button_hovered: Style, container: Style, } fn initialize_ui(mut commands: Commands) { commands.spawn(( Camera2dBundle { ..default() }, UiCameraConfig { show_ui: true }, )); let base_style = Style { border: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(5.0)), flex_direction: FlexDirection::Column, overflow: Overflow::clip(), ..default() }; commands .spawn(NodeBundle { style: Style { width: Val::Percent(50.0), height: Val::Percent(50.0), ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), z_index: ZIndex::Local(100), ..default() }) .with_children(|parent| { { parent .spawn(NodeBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), z_index: ZIndex::Local(20), ..default() }) .with_children(|parent| { // Spawn button container let container = parent .spawn(( NodeBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), z_index: ZIndex::Local(10), ..default() }, AudioWidget, ui::Scroll, ui::Sorting(2), )) .id(); // Spawn widget parent.spawn(( ButtonBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), z_index: ZIndex::Local(15), ..default() }, ui::Title { name: "Audio".into(), }, ui::Collapse { target: container }, ui::Sorting(1), )); }); } }); commands.insert_resource(Styles { button: Style { ..default() }, button_hovered: Style { ..default() }, container: Style { ..default() }, }) } fn import_files( mut events: EventReader, server: Res, mut registry: ResMut, ) { events.iter().for_each(|event| match event { FileDragAndDrop::DroppedFile { path_buf, .. } => { registry.0.push( server.load_untyped( path_buf .clone() .into_os_string() .into_string() .expect("Path converts to string"), ), ); } _ => (), }) } use audio::*; mod audio { use bevy::audio::PlaybackMode; use super::*; #[derive(Debug, Component)] pub struct AudioWidget; pub fn import_audio( mut events: EventReader>, mut commands: Commands, root: Query>, current: Query<(Entity, &Handle)>, server: Res, ) { // TODO: Filter to just audio files events .iter() .filter(|event| match event { AssetEvent::Created { handle } | AssetEvent::Removed { handle } | AssetEvent::Modified { handle } => { if let Some(asset_path) = server.get_handle_path(handle.clone()) { if let Some(extension) = asset_path.path().extension() { extension == "ogg" } else { false } } else { false } } }) .for_each(|event| { let create = |commands: &mut Commands, handle: Handle| { commands.entity(root.single()).with_children(|parent| { let name = { if let Some(asset_path) = server.get_handle_path(handle.clone()) { if let Some(stem) = asset_path.path().file_stem() { if let Some(val) = stem.to_str() { String::from(val) } else { String::from("???") } } else { String::from("???") } } else { String::from("???") } }; let settings = PlaybackSettings { mode: PlaybackMode::Loop, paused: true, ..default() }; parent.spawn(( AudioSourceBundle { source: handle, settings, }, ButtonBundle { style: Style { border: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), ..default() }, border_color: Color::BLACK.into(), ..default() }, ui::Title { name }, )); }); }; let destroy = |commands: &mut Commands, handle: Handle| { if let Some(entity) = current.iter().find_map(|(entity, current)| { if *current == handle { Some(entity) } else { None } }) { commands.entity(entity).despawn_recursive(); } }; match event { AssetEvent::Created { handle } => { info!("Asset created! {:?}", event); create(&mut commands, handle.clone()); } AssetEvent::Removed { handle } => { info!("Asset removed! {:?}", event); destroy(&mut commands, handle.clone()); } AssetEvent::Modified { handle } => { info!("Asset modified! {:?}", event); destroy(&mut commands, handle.clone()); create(&mut commands, handle.clone()); } } }); } pub fn play_audio( events: Query<(Entity, &Interaction, &AudioSink), (With