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.
987 lines
32 KiB
Rust
987 lines
32 KiB
Rust
// Monologue Trees Editor
|
|
//
|
|
// Editor for creating Monologue Trees levels
|
|
//
|
|
// BUGS:
|
|
// * Cannot view scene when selected, WTF
|
|
// * Scene and Animation tabs are whacked
|
|
//
|
|
// TODO:
|
|
// * (medium) Spawn gltf scene
|
|
// * (medium) Load default scene when gltf selected
|
|
// * (medium) Set gltf to active/inactive
|
|
// * (medium) Play individual animation(s)
|
|
// * Only select one at a time.
|
|
// * (hard) Better Colorscheme
|
|
// * (medium) Visual errors for bad GLTFs
|
|
// * (medium) Spawn clicked scene
|
|
// * (medium) Play clicked animation
|
|
// * (easy) Play all animations
|
|
// * (medium) Add fonts similar to Audios based on inspect-fonts
|
|
// * (hard) Add Dialogs (requires text box UI, saving, loading).
|
|
//
|
|
// Asset types:
|
|
// * Audios (done)
|
|
// * Loop individual
|
|
// * Stop all
|
|
// * Gltfs (doing)
|
|
// * Scenes
|
|
// * Animations
|
|
// * Play/Pause all
|
|
// * Fonts
|
|
// * Monologues
|
|
|
|
use bevy::{
|
|
asset::{Asset, Assets},
|
|
asset::{AssetLoader, LoadContext, LoadedAsset},
|
|
audio::PlaybackMode,
|
|
ecs::system::EntityCommands,
|
|
gltf::Gltf,
|
|
prelude::*,
|
|
utils::BoxedFuture,
|
|
};
|
|
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_asset::<Monologue>()
|
|
.init_asset_loader::<MonologueLoader>()
|
|
.add_event::<CustomAssetEvent<Scene>>()
|
|
.add_event::<CustomAssetEvent<AnimationClip>>()
|
|
.add_systems(Startup, initialize_ui)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
import_files,
|
|
audio_ui,
|
|
gltf_ui,
|
|
fonts_ui,
|
|
texts_ui,
|
|
cameras_ui,
|
|
manage_active_gltf,
|
|
manage_gltf_scene_ui,
|
|
manage_gltf_animation_ui,
|
|
scenes_ui,
|
|
animations_ui,
|
|
spawn_scenes,
|
|
manage_camera,
|
|
play_animation,
|
|
play_audio,
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct AssetRegistry(Vec<HandleUntyped>);
|
|
|
|
#[derive(Event)]
|
|
pub enum CustomAssetEvent<T: Asset> {
|
|
Add { handle: Handle<T>, name: String },
|
|
Remove { handle: Handle<T> },
|
|
Clear,
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct TabRoot;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct LevelRoot;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub enum Minimize {
|
|
Open,
|
|
Closed,
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct EditorCamera;
|
|
|
|
fn initialize_ui(mut commands: Commands) {
|
|
// Empty entity for populating the level being edited
|
|
commands.spawn(LevelRoot);
|
|
|
|
commands.spawn((
|
|
Camera3dBundle { ..default() },
|
|
UiCameraConfig { show_ui: true },
|
|
Name::new("Editor Camera"),
|
|
EditorCamera,
|
|
));
|
|
|
|
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 {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(5.0)),
|
|
padding: UiRect::all(Val::Px(5.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(5.0)),
|
|
padding: UiRect::all(Val::Px(5.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,
|
|
&base_style,
|
|
));
|
|
content_containers.push(spawn_tab_container::<AudioWidget>(
|
|
"Audio",
|
|
parent,
|
|
&base_style,
|
|
));
|
|
content_containers.push(spawn_tab_container::<GltfWidget>(
|
|
"Gltf",
|
|
parent,
|
|
&base_style,
|
|
));
|
|
content_containers.push(spawn_tab_container::<SceneWidget>(
|
|
"Scene",
|
|
parent,
|
|
&base_style,
|
|
));
|
|
content_containers.push(spawn_tab_container::<AnimationWidget>(
|
|
"Animation",
|
|
parent,
|
|
&base_style,
|
|
));
|
|
content_containers.push(spawn_tab_container::<CameraWidget>(
|
|
"Camera",
|
|
parent,
|
|
&base_style,
|
|
));
|
|
});
|
|
|
|
// Container for tabs that open/close containers
|
|
parent
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(5.0)),
|
|
padding: UiRect::all(Val::Px(5.0)),
|
|
flex_direction: FlexDirection::Column,
|
|
overflow: Overflow::clip(),
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
ui::Sorting(1),
|
|
))
|
|
.with_children(|parent| {
|
|
let b = ButtonBundle {
|
|
style: Style {
|
|
..base_style.clone()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
};
|
|
content_containers.iter().for_each(|(name, target)| {
|
|
parent.spawn((
|
|
b.clone(),
|
|
ui::Title {
|
|
name: name.clone(),
|
|
..default()
|
|
},
|
|
ui::Collapse { target: *target },
|
|
));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn spawn_tab_container<T: Default + Component>(
|
|
title: &'static str,
|
|
parent: &mut ChildBuilder,
|
|
base_style: &Style,
|
|
) -> (String, Entity) {
|
|
(
|
|
title.into(),
|
|
parent
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
display: Display::None,
|
|
top: Val::Px(0.0),
|
|
..base_style.clone()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
z_index: ZIndex::Local(100),
|
|
..default()
|
|
},
|
|
ui::Title {
|
|
name: title.into(),
|
|
..default()
|
|
},
|
|
T::default(),
|
|
ui::Scroll,
|
|
Interaction::default(),
|
|
))
|
|
.id(),
|
|
)
|
|
}
|
|
|
|
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 super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct AudioWidget;
|
|
|
|
pub fn audio_ui(
|
|
mut events: EventReader<AssetEvent<AudioSource>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<AudioWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<AudioSource>)>,
|
|
server: Res<AssetServer>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|&event| match event {
|
|
AssetEvent::Created { handle }
|
|
| AssetEvent::Removed { handle }
|
|
| AssetEvent::Modified { handle } => {
|
|
has_extensions(&server, handle.clone(), &["ogg"])
|
|
}
|
|
})
|
|
.for_each(|event| match event {
|
|
AssetEvent::Created { handle } => {
|
|
info!("Asset created! {:?}", event);
|
|
let id = create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
get_asset_name(&server, handle.clone()),
|
|
);
|
|
commands.entity(id).insert(AudioSourceBundle {
|
|
source: handle.clone(),
|
|
settings: PlaybackSettings {
|
|
mode: PlaybackMode::Loop,
|
|
paused: true,
|
|
..default()
|
|
},
|
|
});
|
|
}
|
|
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(),
|
|
},
|
|
);
|
|
let id = create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
get_asset_name(&server, handle.clone()),
|
|
);
|
|
commands.entity(id).insert(AudioSourceBundle {
|
|
source: handle.clone(),
|
|
settings: PlaybackSettings {
|
|
mode: PlaybackMode::Loop,
|
|
paused: true,
|
|
..default()
|
|
},
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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 assets::*;
|
|
mod assets {
|
|
|
|
use super::*;
|
|
|
|
pub fn get_asset_name<T: Asset>(server: &AssetServer, handle: Handle<T>) -> String {
|
|
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("???")
|
|
}
|
|
}
|
|
|
|
pub fn create_asset_button<A: Asset, C: Component>(
|
|
root: &Query<Entity, With<C>>,
|
|
commands: &mut Commands,
|
|
target: ui::TargetAsset<A>,
|
|
name: String,
|
|
) -> Entity {
|
|
commands
|
|
.spawn((
|
|
target,
|
|
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, ..default() },
|
|
))
|
|
.set_parent(root.single())
|
|
.id()
|
|
}
|
|
|
|
pub fn create_entity_button<C: Component>(
|
|
root: &Query<Entity, With<C>>,
|
|
commands: &mut Commands,
|
|
target: ui::TargetEntity,
|
|
name: String,
|
|
) -> Entity {
|
|
commands
|
|
.spawn((
|
|
target,
|
|
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, ..default() },
|
|
))
|
|
.set_parent(root.single())
|
|
.id()
|
|
}
|
|
|
|
pub fn destroy_asset_button<A: Asset>(
|
|
current: &Query<(Entity, &ui::TargetAsset<A>)>,
|
|
commands: &mut Commands,
|
|
target: &ui::TargetAsset<A>,
|
|
) {
|
|
if let Some(entity) = current.iter().find_map(|(entity, this)| {
|
|
if this.handle == target.handle {
|
|
Some(entity)
|
|
} else {
|
|
None
|
|
}
|
|
}) {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
|
|
pub fn has_extensions<T: Asset>(
|
|
server: &AssetServer,
|
|
handle: Handle<T>,
|
|
extensions: &[&'static str],
|
|
) -> bool {
|
|
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
|
|
if let Some(extension) = asset_path.path().extension() {
|
|
extensions.iter().any(|&check| check == extension)
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
use gltf::*;
|
|
mod gltf {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct GltfWidget;
|
|
|
|
// TODO: Mark selected gltf as active ~single exclusive~
|
|
pub fn gltf_ui(
|
|
mut events: EventReader<AssetEvent<Gltf>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<GltfWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<Gltf>)>,
|
|
server: Res<AssetServer>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|&event| match event {
|
|
AssetEvent::Created { handle }
|
|
| AssetEvent::Removed { handle }
|
|
| AssetEvent::Modified { handle } => {
|
|
has_extensions(&server, handle.clone(), &["gltf", "glb"])
|
|
}
|
|
})
|
|
.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()),
|
|
);
|
|
}
|
|
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()),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn manage_active_gltf(
|
|
events: Query<
|
|
(Entity, &Parent, &Interaction, Option<&ui::Active>),
|
|
(With<Button>, Changed<Interaction>),
|
|
>,
|
|
children: Query<&Children, With<ui::Active>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(_, _, &interaction, _)| interaction == Interaction::Pressed)
|
|
.for_each(|(entity, parent, _, active_ish)| match active_ish {
|
|
Some(_) => {
|
|
commands.entity(entity).remove::<ui::Active>();
|
|
}
|
|
None => {
|
|
if let Ok(childs) = children.get(parent.get()) {
|
|
childs
|
|
.iter()
|
|
.filter(|&child| *child != entity)
|
|
.for_each(|&child| {
|
|
commands.entity(child).remove::<ui::Active>();
|
|
});
|
|
}
|
|
commands.entity(entity).insert(ui::Active);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn manage_gltf_animation_ui(
|
|
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
mut removed: RemovedComponents<ui::Active>,
|
|
targets_gltf: Query<&ui::TargetAsset<Gltf>>,
|
|
gltfs: Res<Assets<Gltf>>,
|
|
mut animation_clip_events: EventWriter<CustomAssetEvent<AnimationClip>>,
|
|
) {
|
|
removed
|
|
.iter()
|
|
.filter_map(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
|
|
gltfs.get(handle)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.for_each(|gltf| {
|
|
gltf.named_animations
|
|
.iter()
|
|
.for_each(|(animation_name, animation_handle)| {
|
|
info!("Named animation: {:?}", animation_name);
|
|
animation_clip_events.send(CustomAssetEvent::Remove {
|
|
handle: animation_handle.clone(),
|
|
});
|
|
});
|
|
});
|
|
added
|
|
.iter()
|
|
.filter_map(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
|
|
gltfs.get(handle)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.for_each(|gltf| {
|
|
// Populate animations tab
|
|
gltf.named_animations
|
|
.iter()
|
|
.for_each(|(animation_name, animation_handle)| {
|
|
animation_clip_events.send(CustomAssetEvent::Add {
|
|
name: animation_name.clone(),
|
|
handle: animation_handle.clone(),
|
|
})
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn manage_gltf_scene_ui(
|
|
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
mut removed: RemovedComponents<ui::Active>,
|
|
targets_gltf: Query<&ui::TargetAsset<Gltf>>,
|
|
gltfs: Res<Assets<Gltf>>,
|
|
mut scene_events: EventWriter<CustomAssetEvent<Scene>>,
|
|
) {
|
|
removed
|
|
.iter()
|
|
.filter_map(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
|
|
gltfs.get(handle)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.for_each(|gltf| {
|
|
gltf.named_scenes
|
|
.iter()
|
|
.for_each(|(scene_name, scene_handle)| {
|
|
info!("Named scene: {:?}", scene_name);
|
|
scene_events.send(CustomAssetEvent::Remove {
|
|
handle: scene_handle.clone(),
|
|
});
|
|
});
|
|
});
|
|
added
|
|
.iter()
|
|
.filter_map(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
|
|
gltfs.get(handle)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.for_each(|gltf| {
|
|
// Populate scenes tab
|
|
gltf.named_scenes
|
|
.iter()
|
|
.for_each(|(scene_name, scene_handle)| {
|
|
info!("Named scene: {:?}", scene_name);
|
|
scene_events.send(CustomAssetEvent::Add {
|
|
name: scene_name.clone(),
|
|
handle: scene_handle.clone(),
|
|
})
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
// TODO: Mark loaded animation as active
|
|
use scenes::*;
|
|
mod scenes {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct SceneWidget;
|
|
|
|
pub fn scenes_ui(
|
|
mut events: EventReader<CustomAssetEvent<Scene>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<SceneWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<Scene>)>,
|
|
) {
|
|
events.iter().for_each(|event| {
|
|
match event {
|
|
CustomAssetEvent::Add { name, handle } => {
|
|
info!("Asset loading! {:?}({:?})", name, handle);
|
|
// Spawn new tree
|
|
create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
name.clone(),
|
|
);
|
|
}
|
|
CustomAssetEvent::Remove { handle } => {
|
|
destroy_asset_button(
|
|
¤t,
|
|
&mut commands,
|
|
&ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
);
|
|
}
|
|
CustomAssetEvent::Clear => {
|
|
commands.entity(widget.single()).despawn_descendants();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn spawn_scenes(
|
|
events: Query<
|
|
(&Interaction, &ui::TargetAsset<Scene>),
|
|
(With<Button>, Changed<Interaction>),
|
|
>,
|
|
level_root: Query<Entity, With<LevelRoot>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
|
|
.for_each(|(_, ui::TargetAsset { handle })| {
|
|
info!("Spawning {:?}", handle);
|
|
commands
|
|
.entity(level_root.single())
|
|
.with_children(|parent| {
|
|
parent.spawn((handle.clone(), TransformBundle { ..default() }));
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: Play all animations
|
|
// TODO: Mark playing animation as active
|
|
use animations::*;
|
|
mod animations {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct AnimationWidget;
|
|
|
|
pub fn animations_ui(
|
|
mut events: EventReader<CustomAssetEvent<AnimationClip>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<AnimationWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<AnimationClip>)>,
|
|
) {
|
|
events.iter().for_each(|event| {
|
|
match event {
|
|
CustomAssetEvent::Add { name, handle } => {
|
|
info!("Asset loading! {:?}({:?})", name, handle);
|
|
// Spawn new tree
|
|
create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
name.clone(),
|
|
);
|
|
}
|
|
CustomAssetEvent::Remove { handle } => {
|
|
destroy_asset_button(
|
|
¤t,
|
|
&mut commands,
|
|
&ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
);
|
|
}
|
|
CustomAssetEvent::Clear => {
|
|
commands.entity(widget.single()).despawn_descendants();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn play_animation(
|
|
events: Query<
|
|
(&Interaction, &ui::TargetAsset<AnimationClip>),
|
|
(With<Button>, Changed<Interaction>),
|
|
>,
|
|
mut targets: Query<(&mut AnimationPlayer, &Name), With<Transform>>,
|
|
clips: Res<Assets<AnimationClip>>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
|
|
.for_each(|(_, ui::TargetAsset { handle })| {
|
|
let clip = clips.get(handle).expect("Load animation clip");
|
|
targets
|
|
.iter_mut()
|
|
.filter(|(_, name)| clip.compatible_with(name))
|
|
.for_each(|(mut player, _)| {
|
|
player.play(handle.clone()).repeat();
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
use fonts::*;
|
|
mod fonts {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct FontWidget;
|
|
|
|
// TODO: Make each button have the font
|
|
pub fn fonts_ui(
|
|
mut events: EventReader<AssetEvent<Font>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<FontWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<Font>)>,
|
|
server: Res<AssetServer>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|&event| match event {
|
|
AssetEvent::Created { handle }
|
|
| AssetEvent::Removed { handle }
|
|
| AssetEvent::Modified { handle } => {
|
|
has_extensions(&server, handle.clone(), &["ttf", "otf"])
|
|
}
|
|
})
|
|
.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()),
|
|
);
|
|
}
|
|
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()),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
use monologues::*;
|
|
mod monologues {
|
|
use super::*;
|
|
use bevy::reflect::{TypePath, TypeUuid};
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct MonologueWidget;
|
|
|
|
#[derive(Debug, Deserialize, TypeUuid, TypePath, PartialEq)]
|
|
#[uuid = "216a570b-d142-4026-baed-d7feb0250458"]
|
|
pub struct Monologue {
|
|
text: String,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct MonologueLoader;
|
|
|
|
impl AssetLoader for MonologueLoader {
|
|
fn load<'a>(
|
|
&'a self,
|
|
bytes: &'a [u8],
|
|
load_context: &'a mut LoadContext,
|
|
) -> BoxedFuture<'a, Result<(), bevy::asset::Error>> {
|
|
Box::pin(async move {
|
|
load_context.set_default_asset(LoadedAsset::new(
|
|
String::from_utf8(bytes.to_vec()).expect("Convert bytes to String"),
|
|
));
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn extensions(&self) -> &[&str] {
|
|
&[".monologue.txt"]
|
|
}
|
|
}
|
|
|
|
// TODO: Load .txt files for monologues
|
|
pub fn texts_ui(
|
|
mut events: EventReader<AssetEvent<Monologue>>,
|
|
mut commands: Commands,
|
|
widget: Query<Entity, With<MonologueWidget>>,
|
|
current: Query<(Entity, &ui::TargetAsset<Monologue>)>,
|
|
server: Res<AssetServer>,
|
|
) {
|
|
events.iter().for_each(|event| {
|
|
info!("Loading monologue");
|
|
})
|
|
}
|
|
}
|
|
|
|
use cameras::*;
|
|
mod cameras {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct CameraWidget;
|
|
|
|
// TODO: Despawn camera button when camera removed
|
|
pub fn cameras_ui(
|
|
mut events: Query<(Entity, &mut Camera, &Name), (Added<Camera>, Without<EditorCamera>)>,
|
|
widget: Query<Entity, With<CameraWidget>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.iter_mut().for_each(|(entity, mut camera, name)| {
|
|
info!("Camera added {:?} {:?}", entity, name);
|
|
create_entity_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetEntity { entity },
|
|
name.as_str().into(),
|
|
);
|
|
camera.is_active = false;
|
|
})
|
|
}
|
|
|
|
pub fn manage_camera(
|
|
events: Query<(&Interaction, &ui::TargetEntity), Changed<Interaction>>,
|
|
mut cameras: Query<(Entity, &mut Camera)>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
|
|
.for_each(|(_, ui::TargetEntity { entity })| {
|
|
cameras.iter_mut().for_each(|(this_entity, mut camera)| {
|
|
camera.is_active = this_entity == *entity;
|
|
});
|
|
});
|
|
}
|
|
}
|