// Monologue Trees Editor // // Editor for creating Monologue Trees levels // // BUGS: // * Cannot view scene when selected, WTF // * Scene and Animation tabs are whacked // // TODO: // * (medium) Spawn gltf scene // * (medium) Load default scene when gltf selected // * (medium) Set gltf to active/inactive // * (medium) Play individual animation(s) // * Only select one at a time. // * (hard) Better Colorscheme // * (medium) Visual errors for bad GLTFs // * (medium) Spawn clicked scene // * (medium) Play clicked animation // * (easy) Play all animations // * (medium) Add fonts similar to Audios based on inspect-fonts // * (hard) Add Dialogs (requires text box UI, saving, loading). // // Asset types: // * Audios (done) // * Loop individual // * Stop all // * Gltfs (doing) // * Scenes // * Animations // * Play/Pause all // * Fonts // * Monologues use bevy::{ asset::{Asset, Assets}, asset::{AssetLoader, LoadContext, LoadedAsset}, audio::PlaybackMode, gltf::Gltf, input::{keyboard::KeyboardInput, ButtonState}, prelude::*, utils::BoxedFuture, }; 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_asset::() .init_asset_loader::() .add_event::>() .add_event::>() .add_systems(Startup, (initialize_ui, welcome_message)) .add_systems( Update, ( import_files, audio_ui, gltf_ui, fonts_ui, texts_ui, cameras_ui, manage_active_gltf, manage_gltf_scene_ui, manage_gltf_animation_ui, scenes_ui, animations_ui, spawn_scenes, manage_camera, play_animation, play_audio, ), ) .run(); } #[derive(Resource, Default)] pub struct AssetRegistry(Vec); #[derive(Event)] pub enum CustomAssetEvent { Add { handle: Handle, name: String }, Remove { handle: Handle }, Clear, } #[derive(Debug, Component)] pub struct TabRoot; #[derive(Debug, 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((TransformBundle { ..default() }, LevelRoot)); commands.spawn(( Camera3dBundle { ..default() }, UiCameraConfig { show_ui: true }, Name::new("Editor Camera"), EditorCamera, )); 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() }; 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, ui::Select::Single, )); content_containers.push(spawn_tab_container::( "Audio", parent, ui::Select::Multi, )); content_containers.push(spawn_tab_container::( "Gltf", parent, ui::Select::Single, )); content_containers.push(spawn_tab_container::( "Scene", parent, ui::Select::Single, )); content_containers.push(spawn_tab_container::( "Animation", parent, ui::Select::Multi, )); content_containers.push(spawn_tab_container::( "Camera", parent, ui::Select::Single, )); }); // 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| { let b = ButtonBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }; content_containers.iter().for_each(|(name, target)| { parent.spawn(( b.clone(), ui::Title { text: name.clone(), ..default() }, ui::Collapse { target: *target }, )); }); }); }) .id(); 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(), align_items: AlignItems::Center, align_content: AlignContent::Center, justify_content: JustifyContent::SpaceBetween, ..default() }, background_color: Color::ALICE_BLUE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Title { text: "Assets".into(), ..default() }, ui::Minimize { target: container }, ui::Sorting(0), )); }); } fn welcome_message(mut writer: EventWriter) { writer.send(ui::Alert::Info( "Welcome to the Monologue Trees editor!".into(), )); writer.send(ui::Alert::Info( [ "Import assets by dragging and dropping files into the editor", "", "Supported file types (for now):", "* 3D: .gltf, .glb", "* Audio: .ogg", "* Font: .ttf, .otf", ] .join("\n") .into(), )); } fn spawn_tab_container( title: &'static str, parent: &mut ChildBuilder, select: ui::Select, ) -> (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(), select, )) .id(), ) } 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 super::*; #[derive(Debug, Component, Default)] pub struct AudioWidget; pub fn audio_ui( mut events: EventReader>, mut commands: Commands, widget: Query>, current: Query<(Entity, &ui::TargetAsset)>, server: Res, ) { events .iter() .filter(|&event| match event { AssetEvent::Created { handle } | AssetEvent::Removed { handle } | AssetEvent::Modified { handle } => { has_extensions(&server, handle.clone(), &["ogg"]) } }) .for_each(|event| match event { AssetEvent::Created { handle } => { info!("Asset created! {:?}", event); let id = create_asset_button( &widget, &mut commands, ui::TargetAsset { handle: handle.clone(), }, get_asset_name(&server, handle.clone()), None, ); commands.entity(id).insert(AudioSourceBundle { source: handle.clone(), settings: PlaybackSettings { mode: PlaybackMode::Loop, paused: true, ..default() }, }); } 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(), }, ); let id = create_asset_button( &widget, &mut commands, ui::TargetAsset { handle: handle.clone(), }, get_asset_name(&server, handle.clone()), None, ); commands.entity(id).insert(AudioSourceBundle { source: handle.clone(), settings: PlaybackSettings { mode: PlaybackMode::Loop, paused: true, ..default() }, }); } }); } pub fn play_audio( events: Query<(Entity, &Interaction, &AudioSink), (With