// Monologe Trees Editor // // Editor for creating Monologue Trees levels // // BUGS: // * When Handle is loaded, the button for TargetAsset should load as well // * Exported level should preserve active camera // * Picking new GLTF resets audio without resetting buttons // * Error with exported scene w/ animation: // WARN bevy_asset::asset_server: encountered an error while loading an asset: // no registration found for type // `alloc::vec::Vec< // alloc::vec::Vec< // core::option::Option< // bevy_ecs::entity::Entity // >>>` // at output.scn.ron:723:23 // This is the `path_cache` field on an Animation // ... // Also many other components out of the box just straight up don't work... // // TODO: // * edit textbox with actions // * (brutal) export level // * (hard) import level // * (hard) Harden Active Camera // * (medium) Pre-compute animation target entities // * make min/max/close buttons into actions not selects // * (???) Better handle hide/close monologue use bevy::{ asset::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset}, audio::PlaybackMode, core_pipeline::tonemapping::DebandDither, gltf::Gltf, prelude::*, utils::{BoxedFuture, Duration}, }; use monologue_trees::{debug::*, ui}; const WELCOME_MESSAGES: &'static [&'static str] = &[ "Welcome to the Monologue Trees editor!", "Import assets by dragging and dropping files or folders into the editor!", concat!( "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, }, )) .register_type::() .register_type::() .init_resource::() .init_resource::() .add_asset::() .init_asset_loader::() .add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message)) .add_systems(Update, quit.run_if(ui::activated::)) .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, ( gltf_ui, texts_ui, control_active_gltf, show_preview_text, sync_monologue_font, ), ) .add_systems(Update, (fonts_ui, set_active_font)) .add_systems(Startup, reload_assets) .add_systems( Update, ( reload_assets.run_if(ui::activated::), clear_assets.run_if(ui::activated::), ), ) .add_systems( Update, ( point_light_force_shadows, spot_light_force_shadows, directional_light_force_shadows, ), ) .add_systems(Update, clear_level) .add_systems( Update, ( level_ui, load_level, export_level.run_if(ui::activated::), rehydrate_level::, rehydrate_level::, PlaybackSettings>, //rehydrate_level::, AnimationPlayer>, //rehydrate_level::, ), ) .run(); } #[derive(Resource, Default)] pub struct AssetRegistry(Vec); #[derive(Debug, Component)] pub struct TabRoot; #[derive(Debug, Component, Reflect, Default)] #[reflect(Component)] pub struct LevelRoot; #[derive(Debug, Component, Reflect, Default)] #[reflect(Component)] pub struct AudioRoot; #[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(AudioRoot); 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::( "Level", parent, ui::Select::Single, )); 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(), ReloadAssets, ui::Sorting(1), ui::Title { text: "Reload Assets".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ClearAssets, ui::Sorting(2), ui::Title { text: "Clear Assets".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ExportLevel, ui::Sorting(3), ui::Title { text: "Export Level".into(), ..default() }, )); parent.spawn(( simple_button.clone(), ClearLevel, ui::Sorting(3), ui::Title { text: "Clear Level".into(), ..default() }, )); parent.spawn(( simple_button.clone(), QuitAction, ui::Sorting(3), ui::Title { text: "Quit".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(), ) } 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); create_asset_button( &widget, &mut commands, ui::TargetAsset { handle: handle.clone(), }, get_asset_name(&server, handle.clone()), None, ); } 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(), }, ); create_asset_button( &widget, &mut commands, ui::TargetAsset { handle: handle.clone(), }, get_asset_name(&server, handle.clone()), None, ); } }); } pub fn play_audio( events: Query<&ui::TargetAsset, (With