You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

558 lines
20 KiB
Rust

// 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<T> is loaded, the button for TargetAsset<T> 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::<LevelRoot>()
.register_type::<AudioRoot>()
.init_resource::<AssetRegistry>()
.init_resource::<FontInfo>()
.add_asset::<Monologue>()
.init_asset_loader::<MonologueLoader>()
.add_event::<ControlAudio>()
.add_event::<ControlMonologue>()
.add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message))
.add_systems(Update, quit.run_if(ui::activated::<QuitAction>))
.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::<AudioSource>,
ui_inactive::<AudioSource>,
control_audio,
),
)
.add_systems(
Update,
(
gltf_ui,
texts_ui,
control_active_gltf,
control_monologue,
ui_control_monologue,
ui_active::<Monologue>,
ui_inactive::<Monologue>,
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::<ReloadAssets>),
clear_assets.run_if(ui::activated::<ClearAssets>),
),
)
.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::<ExportLevel>),
rehydrate_level::<Visibility, ComputedVisibility>,
rehydrate_level::<Handle<AudioSource>, PlaybackSettings>,
),
)
.add_systems(
Update,
(
sync_asset_buttons::<Font>,
sync_remove_asset_buttons::<Font>,
sync_asset_buttons::<AudioSource>,
sync_remove_asset_buttons::<AudioSource>,
sync_asset_buttons::<Monologue>,
sync_remove_asset_buttons::<Monologue>,
sync_asset_buttons::<Level>,
sync_remove_asset_buttons::<Level>,
sync_asset_buttons::<Gltf>,
sync_remove_asset_buttons::<Gltf>,
sync_asset_buttons::<Scene>,
sync_remove_asset_buttons::<Scene>,
sync_asset_buttons::<AnimationClip>,
sync_remove_asset_buttons::<AnimationClip>,
),
)
.add_systems(
Update,
(
add_timeline_epoch.run_if(ui::activated::<AddEpoch>),
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::<FontWidget>("Font", parent));
content_containers
.push(spawn_tab_container::<MonologueWidget>("Monologue", parent));
content_containers
.push(spawn_tab_container::<AudioWidget>("Audio", parent));
content_containers
.push(spawn_tab_container::<LevelWidget>("Level", parent));
content_containers
.push(spawn_tab_container::<GltfWidget>("Gltf", parent));
content_containers
.push(spawn_tab_container::<SceneWidget>("Scene", parent));
content_containers
.push(spawn_tab_container::<CameraWidget>("Camera", parent));
content_containers
.push(spawn_tab_container::<AnimationWidget>("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<ui::Alert>) {
WELCOME_MESSAGES
.iter()
.for_each(|&msg| writer.send(ui::Alert::Info(msg.into())))
}
fn spawn_tab_container<T: Default + Component>(
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(),
)
}