// Monologe Trees Editor // // Editor for creating Monologue Trees levels // // REFACTOR: // * Monologue -> Event -> Active -> UI // * Scene -> Event -> Active -> UI // * Animation -> Event -> Active -> UI // * Font -> Event -> Active -> UI // * Gltf -> Event -> Active -> UI // // 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 // // 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::ChangeWatcher, gltf::Gltf, prelude::*, utils::Duration}; use monologue_trees::{ debug::*, editor::{ animation::*, asset_sync::*, assets::*, audio::*, camera::*, font::*, gltf::*, level::*, lighting::*, monologue::*, quit::*, reset::*, scene::*, timeline::*, *, }, 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_event::() .add_event::() .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, ui_control_audio, ui_active::, ui_inactive::, control_audio, ), ) .add_systems( Update, ( gltf_ui, texts_ui, control_active_gltf, control_monologue, ui_control_monologue, ui_active::, ui_inactive::, sync_monologue_font, ), ) .add_systems(Update, (fonts_ui, ui_control_font, sync_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>, ), ) .add_systems( Update, ( sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, sync_asset_buttons::, sync_remove_asset_buttons::, ), ) .add_systems( Update, ( add_timeline_epoch.run_if(ui::activated::), set_epoch_gltf, load_epoch_gltf, set_epoch_scene, load_epoch_scene, set_epoch_camera, load_epoch_camera, set_epoch_music, load_epoch_music, set_epoch_monologue, load_epoch_monologue, set_epoch_font, load_epoch_font, set_epoch_sfx, load_epoch_sfx, set_epoch_animations, load_epoch_animations, ), ) .run(); } #[derive(Debug, Component)] pub struct TabRoot; 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() }; // Assets widget 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)); content_containers .push(spawn_tab_container::("Monologue", parent)); content_containers .push(spawn_tab_container::("Audio", parent)); content_containers .push(spawn_tab_container::("Level", parent)); content_containers .push(spawn_tab_container::("Gltf", parent)); content_containers .push(spawn_tab_container::("Scene", parent)); content_containers .push(spawn_tab_container::("Camera", parent)); content_containers .push(spawn_tab_container::("Animation", parent)); }); // 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), )); }); // Actions widget 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), )); }); // Actions widget commands .spawn(NodeBundle { style: Style { bottom: Val::Px(0.0), right: 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() }, ui::Sorting(99), ui::Select::Single, TimelineWidget, )) .with_children(|parent| { // "Add Epoch" button 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, align_items: AlignItems::Center, justify_items: JustifyItems::Center, overflow: Overflow::clip(), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, ui::Select::Action, )) .with_children(|parent| { parent.spawn(( simple_button.clone(), AddEpoch, ui::Title { text: "+".into(), ..default() }, )); }); }) .id(); parent.spawn(( ui::TitleBarBase::new(Color::WHITE).bundle(), ui::Title { text: "Timeline".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, ) -> (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(), )) .id(), ) }