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
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(),
|
|
)
|
|
}
|