// Monologe Trees Editor // // Editor for creating Monologue Trees levels // // BUGS: // // TODO: // * (medium) Select Font -> "Default Font" Resource // * (medium) Pre-compute animation target entities // * (hard) Harden Active Camera // * (brutal) export level // * (hard) import level // * (???) Better handle hide/close monologue use bevy::{ asset::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset}, audio::PlaybackMode, gltf::Gltf, prelude::*, utils::{BoxedFuture, Duration}, }; 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() }) .set(AssetPlugin { watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), ..default() }), DebugInfoPlugin, ui::GameUiPlugin { enable_alerts: true, }, )) .init_resource::() .add_asset::() .init_asset_loader::() .add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message)) .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, play_audio, pause_audio)) .add_systems( Update, ( import_files, gltf_ui, fonts_ui, texts_ui, control_active_gltf, show_preview_text, sync_monologue_font, ), ) .add_systems( Update, ( point_light_force_shadows, spot_light_force_shadows, directional_light_force_shadows, ), ) .add_systems(Update, (clear_level, clear_assets)) .run(); } #[derive(Resource, Default)] pub struct AssetRegistry(Vec); #[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() }; let simple_button = ButtonBundle { style: Style { ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..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::Single, )); content_containers.push(spawn_tab_container::( "Camera", parent, ui::Select::Single, )); content_containers.push(spawn_tab_container::( "Animation", parent, ui::Select::Multi, )); }); // 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), )); }); 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(), ClearAssets, ui::Sorting(1), ui::Title { text: "Clear Assets".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ClearLevel, ui::Sorting(2), ui::Title { text: "Reset Level".into(), ..default() }, )); }) .id(); parent.spawn(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Actions".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<&AudioSink, (With