|
|
|
|
@ -3,6 +3,21 @@
|
|
|
|
|
// Editor for creating Monologue Trees levels
|
|
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
// * Error with exported scene w/ animation:
|
|
|
|
|
// WARN bevy_asset::asset_server: encountered an error while loading an asset:
|
|
|
|
|
// no registration found for type
|
|
|
|
|
// `alloc::vec::Vec<
|
|
|
|
|
// alloc::vec::Vec<
|
|
|
|
|
// core::option::Option<
|
|
|
|
|
// bevy_ecs::entity::Entity
|
|
|
|
|
// >>>`
|
|
|
|
|
// at output.scn.ron:723:23
|
|
|
|
|
// This is the `path_cache` field on an Animation
|
|
|
|
|
// ...
|
|
|
|
|
// Also many other components out of the box just straight up don't work...
|
|
|
|
|
//
|
|
|
|
|
// TODO:
|
|
|
|
|
// * edit textbox with actions
|
|
|
|
|
@ -16,6 +31,7 @@
|
|
|
|
|
use bevy::{
|
|
|
|
|
asset::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset},
|
|
|
|
|
audio::PlaybackMode,
|
|
|
|
|
core_pipeline::tonemapping::DebandDither,
|
|
|
|
|
gltf::Gltf,
|
|
|
|
|
prelude::*,
|
|
|
|
|
utils::{BoxedFuture, Duration},
|
|
|
|
|
@ -56,11 +72,13 @@ fn main() {
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
.register_type::<LevelRoot>()
|
|
|
|
|
.register_type::<AudioRoot>()
|
|
|
|
|
.init_resource::<AssetRegistry>()
|
|
|
|
|
.init_resource::<FontInfo>()
|
|
|
|
|
.add_asset::<Monologue>()
|
|
|
|
|
.init_asset_loader::<MonologueLoader>()
|
|
|
|
|
.add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message))
|
|
|
|
|
.add_systems(Update, quit.run_if(ui::activated::<QuitAction>))
|
|
|
|
|
.add_systems(
|
|
|
|
|
Update,
|
|
|
|
|
(
|
|
|
|
|
@ -111,7 +129,10 @@ fn main() {
|
|
|
|
|
level_ui,
|
|
|
|
|
load_level,
|
|
|
|
|
export_level.run_if(ui::activated::<ExportLevel>),
|
|
|
|
|
rehydrate_level,
|
|
|
|
|
rehydrate_level::<Visibility, ComputedVisibility>,
|
|
|
|
|
rehydrate_level::<Handle<AudioSource>, PlaybackSettings>,
|
|
|
|
|
//rehydrate_level::<Handle<AnimationClip>, AnimationPlayer>,
|
|
|
|
|
//rehydrate_level::<DebandDither, Camera3d>,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.run();
|
|
|
|
|
@ -127,12 +148,17 @@ pub struct TabRoot;
|
|
|
|
|
#[reflect(Component)]
|
|
|
|
|
pub struct LevelRoot;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Component, Reflect, Default)]
|
|
|
|
|
#[reflect(Component)]
|
|
|
|
|
pub struct AudioRoot;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Component)]
|
|
|
|
|
pub struct EditorCamera;
|
|
|
|
|
|
|
|
|
|
fn initialize_ui(mut commands: Commands) {
|
|
|
|
|
// Empty entity for populating the level being edited
|
|
|
|
|
commands.spawn((SpatialBundle { ..default() }, LevelRoot::default()));
|
|
|
|
|
commands.spawn((SpatialBundle { ..default() }, LevelRoot));
|
|
|
|
|
commands.spawn(AudioRoot);
|
|
|
|
|
|
|
|
|
|
commands.spawn((
|
|
|
|
|
Camera2dBundle { ..default() },
|
|
|
|
|
@ -372,7 +398,16 @@ fn initialize_ui(mut commands: Commands) {
|
|
|
|
|
ClearLevel,
|
|
|
|
|
ui::Sorting(3),
|
|
|
|
|
ui::Title {
|
|
|
|
|
text: "Reset Level".into(),
|
|
|
|
|
text: "Clear Level".into(),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
parent.spawn((
|
|
|
|
|
simple_button.clone(),
|
|
|
|
|
QuitAction,
|
|
|
|
|
ui::Sorting(3),
|
|
|
|
|
ui::Title {
|
|
|
|
|
text: "Quit".into(),
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
@ -446,7 +481,7 @@ mod audio {
|
|
|
|
|
events.iter().for_each(|event| match event {
|
|
|
|
|
AssetEvent::Created { handle } => {
|
|
|
|
|
info!("Asset created! {:?}", event);
|
|
|
|
|
let id = create_asset_button(
|
|
|
|
|
create_asset_button(
|
|
|
|
|
&widget,
|
|
|
|
|
&mut commands,
|
|
|
|
|
ui::TargetAsset {
|
|
|
|
|
@ -455,14 +490,6 @@ mod audio {
|
|
|
|
|
get_asset_name(&server, handle.clone()),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
commands.entity(id).insert(AudioSourceBundle {
|
|
|
|
|
source: handle.clone(),
|
|
|
|
|
settings: PlaybackSettings {
|
|
|
|
|
mode: PlaybackMode::Loop,
|
|
|
|
|
paused: true,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
AssetEvent::Removed { handle } => {
|
|
|
|
|
info!("Asset removed! {:?}", event);
|
|
|
|
|
@ -483,7 +510,7 @@ mod audio {
|
|
|
|
|
handle: handle.clone(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let id = create_asset_button(
|
|
|
|
|
create_asset_button(
|
|
|
|
|
&widget,
|
|
|
|
|
&mut commands,
|
|
|
|
|
ui::TargetAsset {
|
|
|
|
|
@ -492,29 +519,55 @@ mod audio {
|
|
|
|
|
get_asset_name(&server, handle.clone()),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
commands.entity(id).insert(AudioSourceBundle {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn play_audio(
|
|
|
|
|
events: Query<&ui::TargetAsset<AudioSource>, (With<Button>, Added<ui::Active>)>,
|
|
|
|
|
root: Query<Entity, With<AudioRoot>>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
events.iter().for_each(|ui::TargetAsset { handle }| {
|
|
|
|
|
let root = if let Ok(entity) = root.get_single() {
|
|
|
|
|
entity
|
|
|
|
|
} else {
|
|
|
|
|
commands.spawn(AudioRoot).id()
|
|
|
|
|
};
|
|
|
|
|
commands.entity(root).with_children(|parent| {
|
|
|
|
|
parent.spawn(AudioSourceBundle {
|
|
|
|
|
source: handle.clone(),
|
|
|
|
|
settings: PlaybackSettings {
|
|
|
|
|
mode: PlaybackMode::Loop,
|
|
|
|
|
paused: true,
|
|
|
|
|
paused: false,
|
|
|
|
|
..default()
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn play_audio(events: Query<&AudioSink, (With<Button>, Added<ui::Active>)>) {
|
|
|
|
|
events.iter().for_each(|sink| {
|
|
|
|
|
sink.play();
|
|
|
|
|
});
|
|
|
|
|
pub fn pause_audio(
|
|
|
|
|
mut events: RemovedComponents<ui::Active>,
|
|
|
|
|
targets: Query<&ui::TargetAsset<AudioSource>>,
|
|
|
|
|
sources: Query<(Entity, &Handle<AudioSource>), With<AudioSink>>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
// Iterate over the audio button events
|
|
|
|
|
events.iter().for_each(|entity| {
|
|
|
|
|
// Get the handle for the audio source we want to despawn
|
|
|
|
|
if let Ok(ui::TargetAsset { handle }) = targets.get(entity) {
|
|
|
|
|
if let Some(entity) = sources.iter().find_map(|(entity, source_handle)| {
|
|
|
|
|
if source_handle == handle {
|
|
|
|
|
Some(entity)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn pause_audio(mut events: RemovedComponents<ui::Active>, sinks: Query<&AudioSink>) {
|
|
|
|
|
events
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|entity| sinks.get(entity).ok())
|
|
|
|
|
.for_each(|sink| sink.stop());
|
|
|
|
|
}) {
|
|
|
|
|
commands.entity(entity).despawn_recursive();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -710,7 +763,9 @@ mod gltf {
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
events.iter().for_each(|_| {
|
|
|
|
|
commands.entity(root.single()).despawn_descendants();
|
|
|
|
|
root.iter().for_each(|entity| {
|
|
|
|
|
commands.entity(entity).despawn_descendants();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -1369,7 +1424,9 @@ mod reset {
|
|
|
|
|
actives.iter().for_each(|entity| {
|
|
|
|
|
commands.entity(entity).remove::<ui::Active>();
|
|
|
|
|
});
|
|
|
|
|
commands.entity(root.single()).despawn_descendants();
|
|
|
|
|
root.iter().for_each(|entity| {
|
|
|
|
|
commands.entity(entity).despawn_descendants();
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1411,9 +1468,6 @@ mod reset {
|
|
|
|
|
|
|
|
|
|
pub use level::*;
|
|
|
|
|
mod level {
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::Write;
|
|
|
|
|
|
|
|
|
|
use bevy::tasks::IoTaskPool;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
@ -1487,7 +1541,9 @@ mod level {
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
events.iter().for_each(|ui::TargetAsset { handle }| {
|
|
|
|
|
commands.entity(root.single()).despawn_recursive();
|
|
|
|
|
root.iter().for_each(|entity| {
|
|
|
|
|
commands.entity(entity).despawn_recursive();
|
|
|
|
|
});
|
|
|
|
|
commands.spawn(DynamicSceneBundle {
|
|
|
|
|
scene: handle.clone(),
|
|
|
|
|
..default()
|
|
|
|
|
@ -1496,7 +1552,8 @@ mod level {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn export_level(
|
|
|
|
|
root: Query<Entity, With<LevelRoot>>,
|
|
|
|
|
level_root: Query<Entity, With<LevelRoot>>,
|
|
|
|
|
audio_root: Query<Entity, With<AudioRoot>>,
|
|
|
|
|
children: Query<&Children>,
|
|
|
|
|
world: &World,
|
|
|
|
|
) {
|
|
|
|
|
@ -1505,31 +1562,48 @@ mod level {
|
|
|
|
|
|
|
|
|
|
builder.deny_all_resources();
|
|
|
|
|
|
|
|
|
|
// builder.allow_all();
|
|
|
|
|
builder.deny::<ComputedVisibility>();
|
|
|
|
|
// Exclude computed visibility
|
|
|
|
|
builder.deny_all();
|
|
|
|
|
|
|
|
|
|
// Level administrivia
|
|
|
|
|
builder.allow::<LevelRoot>();
|
|
|
|
|
builder.allow::<AudioRoot>();
|
|
|
|
|
|
|
|
|
|
// Scene components
|
|
|
|
|
builder.allow::<Handle<Scene>>();
|
|
|
|
|
builder.allow::<Visibility>();
|
|
|
|
|
|
|
|
|
|
// Spatial components
|
|
|
|
|
builder.allow::<Transform>();
|
|
|
|
|
builder.allow::<GlobalTransform>();
|
|
|
|
|
builder.allow::<Visibility>();
|
|
|
|
|
|
|
|
|
|
// Audio components
|
|
|
|
|
builder.allow::<Handle<AudioSource>>();
|
|
|
|
|
builder.allow::<PlaybackSettings>();
|
|
|
|
|
|
|
|
|
|
root.iter().for_each(|r| {
|
|
|
|
|
// Text components
|
|
|
|
|
builder.allow::<Handle<Font>>();
|
|
|
|
|
builder.allow::<Handle<Monologue>>();
|
|
|
|
|
|
|
|
|
|
level_root.iter().for_each(|level| {
|
|
|
|
|
// Extract the level root
|
|
|
|
|
builder.extract_entity(r);
|
|
|
|
|
builder.extract_entity(level);
|
|
|
|
|
|
|
|
|
|
match children.get(r) {
|
|
|
|
|
Ok(cs) => {
|
|
|
|
|
builder.extract_entities(cs.iter().map(|&entity| entity));
|
|
|
|
|
if let Ok(kids) = children.get(level) {
|
|
|
|
|
builder.extract_entities(kids.into_iter().map(|&e| e));
|
|
|
|
|
} else {
|
|
|
|
|
warn!("Level is empty!");
|
|
|
|
|
}
|
|
|
|
|
Err(e) => warn!("Empty level! {:?}", e),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
audio_root.iter().for_each(|audio| {
|
|
|
|
|
// Extract the level root
|
|
|
|
|
builder.extract_entity(audio);
|
|
|
|
|
|
|
|
|
|
if let Ok(kids) = children.get(audio) {
|
|
|
|
|
builder.extract_entities(kids.into_iter().map(|&e| e));
|
|
|
|
|
} else {
|
|
|
|
|
warn!("Audio is empty!");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -1542,21 +1616,32 @@ mod level {
|
|
|
|
|
IoTaskPool::get()
|
|
|
|
|
.spawn(async move {
|
|
|
|
|
// Write the scene RON data to file
|
|
|
|
|
File::create(format!("assets/output.scn.ron"))
|
|
|
|
|
.and_then(|mut file| file.write(serialized.as_bytes()))
|
|
|
|
|
std::fs::write(format!("assets/output.scn.ron"), serialized.as_bytes())
|
|
|
|
|
.expect("Error while writing scene to file");
|
|
|
|
|
})
|
|
|
|
|
.detach();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn rehydrate_level(
|
|
|
|
|
events: Query<Entity, (Added<Visibility>, Without<ComputedVisibility>)>,
|
|
|
|
|
pub fn rehydrate_level<W: Component, WO: Component + Default>(
|
|
|
|
|
events: Query<Entity, (Added<W>, Without<WO>)>,
|
|
|
|
|
mut commands: Commands,
|
|
|
|
|
) {
|
|
|
|
|
events.iter().for_each(|entity| {
|
|
|
|
|
commands
|
|
|
|
|
.entity(entity)
|
|
|
|
|
.insert(ComputedVisibility::default());
|
|
|
|
|
commands.entity(entity).insert(WO::default());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use quit::*;
|
|
|
|
|
mod quit {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
use bevy::app::AppExit;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
|
|
|
pub struct QuitAction;
|
|
|
|
|
|
|
|
|
|
pub fn quit(mut exit: EventWriter<AppExit>) {
|
|
|
|
|
exit.send(AppExit);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|