// Monologe Trees Editor // // Editor for creating Monologue Trees levels // // BUGS: // * Camera order ambiguity // * Multi-GLTF UX is bad. // * Consider GLTF hierarchy (GLTF1 > Scene1a/Scene1b, GlTF2 > Scene2a/Scene2b, etc) // * Easy despawn when de-selecting gltf // // TODO: // * (easy) Play all animations // * (easy) Clear button to wipe spawned scene // * (brutal) export level // * (hard) import level // // Asset types: // * Audios (done) // * Loop individual (done) // * Gltfs (doing) // * Scenes // * Animations // * Play/Pause all // * Fonts (done) // * Monologues (done) use bevy::{ asset::{Asset, Assets}, asset::{AssetLoader, LoadContext, LoadedAsset}, audio::PlaybackMode, gltf::Gltf, prelude::*, utils::BoxedFuture, }; use monologue_trees::{debug::*, ui}; const WELCOME_MESSAGES: &'static [&'static str] = &[ "Welcome to the Monologue Trees editor!", concat!( "Import assets by dragging and dropping files into the editor\n", "\n", "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() }), DebugInfoPlugin, ui::GameUiPlugin, )) .init_resource::() .add_asset::() .init_asset_loader::() .add_event::>() .add_event::>() .add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message)) .add_systems( Update, ( manage_gltf_animation_ui, init_animations_ui, animations_ui, play_all_animations, play_animation, ), ) .add_systems( Update, (manage_gltf_scene_ui, scenes_ui, control_active_scenes), ) .add_systems( Update, ( cameras_ui, manage_active_camera, control_active_camera, fallback_camera, ), ) .add_systems(Update, (audio_ui, play_audio)) .add_systems( Update, ( import_files, gltf_ui, fonts_ui, texts_ui, manage_active_gltf, show_preview_text, sync_monologue_font, ), ) .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((SpatialBundle { ..default() }, LevelRoot)); 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() }; 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::( "Monologue", 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::Multi, )); 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(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Assets".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, 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().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