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.

392 lines
14 KiB
Rust

// Monologue Trees Editor
//
// Editor for creating Monologue Trees levels
//
// TODO:
// * Tree Organization: GLTF contains Animations and Scenes
// * Camera can only select one at a time.
// * (hard) Better Colorscheme
// * (medium) Visual errors for bad GLTFs
// * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras)
// * (medium) Show/hide entire UI
// * (medium) Spawn clicked scene
// * (medium) Play clicked animation
// * (idea) Use enum instead of markers for exclusive UI
// * (medium) Add fonts similar to Audios based on inspect-fonts
// * (hard) Add Dialogs (requires text box UI, saving, loading)
use bevy::{
asset::{AssetPath, Assets},
gltf::Gltf,
input::{keyboard::KeyboardInput, ButtonState},
prelude::*,
utils::HashSet,
};
use monologue_trees::{debug::*, ui};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Monologue Trees Editor".into(),
resolution: (640., 480.).into(),
..default()
}),
..default()
}),
DebugInfoPlugin,
ui::GameUiPlugin,
))
.init_resource::<AssetRegistry>()
.add_systems(Startup, initialize_ui)
.add_systems(
Update,
(import_files, import_audio, import_gltf, play_audio),
)
.run();
}
#[derive(Resource, Default)]
struct AssetRegistry(Vec<HandleUntyped>);
#[derive(Resource)]
struct Styles {
button: Style,
button_hovered: Style,
container: Style,
}
fn initialize_ui(mut commands: Commands) {
commands.spawn((
Camera2dBundle { ..default() },
UiCameraConfig { show_ui: true },
));
let base_style = Style {
border: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(5.0)),
overflow: Overflow::clip(),
flex_direction: FlexDirection::Column,
..default()
};
commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(50.0),
height: Val::Percent(50.0),
flex_direction: FlexDirection::Row,
..base_style.clone()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
z_index: ZIndex::Local(100),
..default()
})
.with_children(|parent| {
spawn_tabtree::<AudioWidget>(parent, "Audio".into(), &base_style);
spawn_tabtree::<GltfWidget>(parent, "Gltf".into(), &base_style);
});
commands.insert_resource(Styles {
button: Style { ..default() },
button_hovered: Style { ..default() },
container: Style { ..default() },
})
}
fn spawn_tabtree<T: Default + Component>(
parent: &mut ChildBuilder,
title: String,
base_style: &Style,
) {
parent
.spawn((
NodeBundle {
style: Style {
..base_style.clone()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
z_index: ZIndex::Local(20),
..default()
},
Interaction::default(),
))
.with_children(|parent| {
// Spawn button container
let container = parent
.spawn((
NodeBundle {
style: Style {
..base_style.clone()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
z_index: ZIndex::Local(10),
..default()
},
T::default(),
ui::Scroll,
Interaction::default(),
ui::Sorting(2),
))
.id();
// Spawn widget
parent.spawn((
ButtonBundle {
style: Style {
..base_style.clone()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
z_index: ZIndex::Local(15),
..default()
},
ui::Title { name: title },
ui::Collapse { target: container },
ui::Sorting(1),
));
});
}
fn import_files(
mut events: EventReader<FileDragAndDrop>,
server: Res<AssetServer>,
mut registry: ResMut<AssetRegistry>,
) {
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 bevy::audio::PlaybackMode;
use super::*;
#[derive(Debug, Component, Default)]
pub struct AudioWidget;
pub fn import_audio(
mut events: EventReader<AssetEvent<AudioSource>>,
mut commands: Commands,
root: Query<Entity, With<AudioWidget>>,
current: Query<(Entity, &Handle<AudioSource>)>,
server: Res<AssetServer>,
) {
events
.iter()
.filter(|&event| match event {
AssetEvent::Created { handle }
| AssetEvent::Removed { handle }
| AssetEvent::Modified { handle } => {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(extension) = asset_path.path().extension() {
extension == "ogg"
} else {
false
}
} else {
false
}
}
})
.for_each(|event| {
let create = |commands: &mut Commands, handle: Handle<AudioSource>| {
commands.entity(root.single()).with_children(|parent| {
let name = {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(stem) = asset_path.path().file_stem() {
if let Some(val) = stem.to_str() {
String::from(val)
} else {
String::from("???")
}
} else {
String::from("???")
}
} else {
String::from("???")
}
};
let settings = PlaybackSettings {
mode: PlaybackMode::Loop,
paused: true,
..default()
};
parent.spawn((
AudioSourceBundle {
source: handle,
settings,
},
ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(5.0)),
..default()
},
border_color: Color::BLACK.into(),
..default()
},
ui::Title { name },
));
});
};
let destroy = |commands: &mut Commands, handle: Handle<AudioSource>| {
if let Some(entity) = current.iter().find_map(|(entity, current)| {
if *current == handle {
Some(entity)
} else {
None
}
}) {
commands.entity(entity).despawn_recursive();
}
};
match event {
AssetEvent::Created { handle } => {
info!("Asset created! {:?}", event);
create(&mut commands, handle.clone());
}
AssetEvent::Removed { handle } => {
info!("Asset removed! {:?}", event);
destroy(&mut commands, handle.clone());
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy(&mut commands, handle.clone());
create(&mut commands, handle.clone());
}
}
});
}
pub fn play_audio(
events: Query<(Entity, &Interaction, &AudioSink), (With<Button>, Changed<Interaction>)>,
mut commands: Commands,
) {
events
.iter()
.filter(|(_, &interaction, _)| interaction == Interaction::Pressed)
.for_each(|(entity, _, sink)| {
sink.toggle();
if sink.is_paused() {
commands.entity(entity).remove::<ui::Active>();
} else {
commands.entity(entity).insert(ui::Active);
}
});
}
}
use gltf::*;
mod gltf {
use super::*;
#[derive(Debug, Component, Default)]
pub struct GltfWidget;
pub fn import_gltf(
mut events: EventReader<AssetEvent<Gltf>>,
mut commands: Commands,
root: Query<Entity, With<GltfWidget>>,
current: Query<(Entity, &Handle<Gltf>)>,
server: Res<AssetServer>,
) {
events
.iter()
.filter(|&event| match event {
AssetEvent::Created { handle }
| AssetEvent::Removed { handle }
| AssetEvent::Modified { handle } => {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(extension) = asset_path.path().extension() {
extension == "gltf" || extension == "glb"
} else {
false
}
} else {
false
}
}
})
.for_each(|event| {
let create = |commands: &mut Commands, handle: Handle<Gltf>| {
commands.entity(root.single()).with_children(|parent| {
let name = {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(stem) = asset_path.path().file_stem() {
if let Some(val) = stem.to_str() {
String::from(val)
} else {
String::from("???")
}
} else {
String::from("???")
}
} else {
String::from("???")
}
};
parent.spawn((
handle,
ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(5.0)),
padding: UiRect::all(Val::Px(5.0)),
..default()
},
border_color: Color::BLACK.into(),
..default()
},
ui::Title { name },
));
});
};
let destroy = |commands: &mut Commands, handle: Handle<Gltf>| {
if let Some(entity) = current.iter().find_map(|(entity, current)| {
if *current == handle {
Some(entity)
} else {
None
}
}) {
commands.entity(entity).despawn_recursive();
}
};
match event {
AssetEvent::Created { handle } => {
info!("Asset created! {:?}", event);
create(&mut commands, handle.clone());
}
AssetEvent::Removed { handle } => {
info!("Asset removed! {:?}", event);
destroy(&mut commands, handle.clone());
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy(&mut commands, handle.clone());
create(&mut commands, handle.clone());
}
}
});
}
}