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.
1511 lines
52 KiB
Rust
1511 lines
52 KiB
Rust
// Monologe Trees Editor
|
|
//
|
|
// Editor for creating Monologue Trees levels
|
|
//
|
|
// BUGS:
|
|
//
|
|
// TODO:
|
|
// * re-load assets dir when new asset is copied
|
|
// * copy-and-reload drag-and-drop dirs
|
|
// * font resource for monologues
|
|
// * edit textbox with actions
|
|
// * run_if logic for actions where possible
|
|
// * make min/max/close buttons into actions not selects
|
|
// * (medium) Select Font -> "Default Font" Resource
|
|
// * (medium) Pre-compute animation target entities
|
|
// * (hard) Harden Active Camera
|
|
// * (brutal) export level
|
|
// * (hard) import level
|
|
// * (???) Better handle hide/close monologue
|
|
|
|
use bevy::{
|
|
asset::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset},
|
|
audio::PlaybackMode,
|
|
gltf::Gltf,
|
|
prelude::*,
|
|
utils::{BoxedFuture, Duration},
|
|
};
|
|
use monologue_trees::{debug::*, 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,
|
|
},
|
|
))
|
|
.init_resource::<AssetRegistry>()
|
|
.insert_resource(AssetsDir::new("assets/scratch"))
|
|
.init_resource::<FontInfo>()
|
|
.add_event::<ImportAsset>()
|
|
.add_asset::<Monologue>()
|
|
.init_asset_loader::<MonologueLoader>()
|
|
.add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message))
|
|
.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, play_audio, pause_audio))
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
gltf_ui,
|
|
texts_ui,
|
|
control_active_gltf,
|
|
show_preview_text,
|
|
sync_monologue_font,
|
|
),
|
|
)
|
|
.add_systems(Update, (fonts_ui, set_active_font))
|
|
.add_systems(Startup, reload_assets)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
init_assets_dir,
|
|
import_assets,
|
|
import_file,
|
|
import_folder,
|
|
reload_assets.run_if(ui::event::<ImportAsset>),
|
|
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)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct AssetRegistry(Vec<HandleUntyped>);
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct TabRoot;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct LevelRoot;
|
|
|
|
#[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));
|
|
|
|
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()
|
|
};
|
|
|
|
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,
|
|
ui::Select::Single,
|
|
));
|
|
content_containers.push(spawn_tab_container::<MonologueWidget>(
|
|
"Monologue",
|
|
parent,
|
|
ui::Select::Single,
|
|
));
|
|
content_containers.push(spawn_tab_container::<AudioWidget>(
|
|
"Audio",
|
|
parent,
|
|
ui::Select::Multi,
|
|
));
|
|
content_containers.push(spawn_tab_container::<GltfWidget>(
|
|
"Gltf",
|
|
parent,
|
|
ui::Select::Single,
|
|
));
|
|
content_containers.push(spawn_tab_container::<SceneWidget>(
|
|
"Scene",
|
|
parent,
|
|
ui::Select::Single,
|
|
));
|
|
content_containers.push(spawn_tab_container::<CameraWidget>(
|
|
"Camera",
|
|
parent,
|
|
ui::Select::Single,
|
|
));
|
|
content_containers.push(spawn_tab_container::<AnimationWidget>(
|
|
"Animation",
|
|
parent,
|
|
ui::Select::Multi,
|
|
));
|
|
});
|
|
|
|
// 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),
|
|
));
|
|
});
|
|
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(),
|
|
ClearAssets,
|
|
ui::Sorting(1),
|
|
ui::Title {
|
|
text: "Clear Assets".into(),
|
|
..default()
|
|
},
|
|
));
|
|
parent.spawn((
|
|
simple_button.clone(),
|
|
ClearLevel,
|
|
ui::Sorting(2),
|
|
ui::Title {
|
|
text: "Reset Level".into(),
|
|
..default()
|
|
},
|
|
));
|
|
})
|
|
.id();
|
|
parent.spawn((
|
|
ui::TitleBarBase::new(Color::WHITE).bundle(),
|
|
ui::Title {
|
|
text: "Actions".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,
|
|
select: ui::Select,
|
|
) -> (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(),
|
|
select,
|
|
))
|
|
.id(),
|
|
)
|
|
}
|
|
|
|
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().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()),
|
|
None,
|
|
);
|
|
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()),
|
|
None,
|
|
);
|
|
commands.entity(id).insert(AudioSourceBundle {
|
|
source: handle.clone(),
|
|
settings: PlaybackSettings {
|
|
mode: PlaybackMode::Loop,
|
|
paused: true,
|
|
..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>, sinks: Query<&AudioSink>) {
|
|
events
|
|
.iter()
|
|
.filter_map(|entity| sinks.get(entity).ok())
|
|
.for_each(|sink| sink.stop());
|
|
}
|
|
}
|
|
|
|
use assets::*;
|
|
mod assets {
|
|
use std::{
|
|
fs::DirBuilder,
|
|
fs::{copy, create_dir_all, read_dir},
|
|
path::PathBuf,
|
|
};
|
|
|
|
use bevy::tasks::IoTaskPool;
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, Resource)]
|
|
pub struct AssetsDir {
|
|
pub root: PathBuf,
|
|
}
|
|
|
|
impl AssetsDir {
|
|
pub fn new(root: &str) -> Self {
|
|
AssetsDir {
|
|
root: PathBuf::from(root)
|
|
.canonicalize()
|
|
.expect("Setting assets dir"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn init_assets_dir(assets_dir: Res<AssetsDir>) {
|
|
if assets_dir.is_added() || assets_dir.is_changed() {
|
|
let d = assets_dir.root.clone();
|
|
IoTaskPool::get()
|
|
.spawn(async move {
|
|
match DirBuilder::new().create(d) {
|
|
Ok(_) => info!("Created assets directory"),
|
|
Err(e) => warn!("Error creating assets directory {:?}", e),
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Event)]
|
|
pub enum ImportAsset {
|
|
File(PathBuf),
|
|
Dir(PathBuf),
|
|
}
|
|
|
|
pub fn import_assets(
|
|
mut events: EventReader<FileDragAndDrop>,
|
|
mut writer: EventWriter<ImportAsset>,
|
|
) {
|
|
events.iter().for_each(|event| match event {
|
|
FileDragAndDrop::DroppedFile { path_buf, .. } => {
|
|
if path_buf.is_file() {
|
|
writer.send(ImportAsset::File(path_buf.clone()));
|
|
} else if path_buf.is_dir() {
|
|
writer.send(ImportAsset::Dir(path_buf.clone()));
|
|
} else {
|
|
warn!("Could not identify filetype for {:?}", path_buf)
|
|
}
|
|
}
|
|
_ => (),
|
|
})
|
|
}
|
|
|
|
pub fn import_file(mut events: EventReader<ImportAsset>, assets_dir: Res<AssetsDir>) {
|
|
events
|
|
.iter()
|
|
.filter_map(|event| match event {
|
|
ImportAsset::File(path) => Some(path.clone()),
|
|
_ => None,
|
|
})
|
|
.for_each(|path| {
|
|
let fname = path.file_name().expect("PathBuf should have file name");
|
|
let newpath = PathBuf::from(assets_dir.root.clone()).join(fname);
|
|
IoTaskPool::get()
|
|
.spawn(async move {
|
|
if path == newpath {
|
|
info!("Skipping copying {:?} to itself {:?}", path, newpath);
|
|
} else {
|
|
match copy(path, newpath) {
|
|
Ok(_) => info!("Created assets directory"),
|
|
Err(e) => warn!("Error creating assets directory {:?}", e),
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
});
|
|
}
|
|
|
|
fn copy_dir(from: PathBuf, to: PathBuf) -> std::io::Result<()> {
|
|
if let Ok(entries) = read_dir(from) {
|
|
for entry in entries.filter_map(|e| e.ok()) {
|
|
if entry.path().is_file() {
|
|
let from_file = entry.path();
|
|
let to_file = to.join(entry.file_name());
|
|
copy(from_file, to_file).map(|_| ())?;
|
|
} else if entry.path().is_dir() {
|
|
info!("Recursive");
|
|
let target = to.join(entry.file_name());
|
|
create_dir_all(target.clone())?;
|
|
copy_dir(entry.path(), target.clone())?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn import_folder(mut events: EventReader<ImportAsset>, assets_dir: Res<AssetsDir>) {
|
|
events
|
|
.iter()
|
|
.filter_map(|event| match event {
|
|
ImportAsset::Dir(path) => Some(path.clone()),
|
|
_ => None,
|
|
})
|
|
.for_each(|path| {
|
|
let fname = path.file_name().expect("PathBuf should have file name");
|
|
let newpath = PathBuf::from(assets_dir.root.clone()).join(fname);
|
|
info!("copying folder {:?} to {:?}", path.clone(), newpath);
|
|
IoTaskPool::get()
|
|
.spawn(async move {
|
|
if path.clone() == newpath {
|
|
info!("Skipping copying {:?} to itself {:?}", path, newpath);
|
|
} else {
|
|
match copy_dir(path.clone(), newpath) {
|
|
Ok(_) => info!("Created assets directory"),
|
|
Err(e) => warn!("Error creating assets directory {:?}", e),
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
});
|
|
}
|
|
|
|
pub fn reload_assets(
|
|
server: Res<AssetServer>,
|
|
mut registry: ResMut<AssetRegistry>,
|
|
assets_dir: Res<AssetsDir>,
|
|
) {
|
|
registry.0 = server
|
|
.load_folder(assets_dir.root.clone())
|
|
.expect("Reload assets folder");
|
|
}
|
|
|
|
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,
|
|
font: Option<Handle<Font>>,
|
|
) -> Entity {
|
|
commands
|
|
.spawn((
|
|
target,
|
|
ButtonBundle {
|
|
style: Style {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
..default()
|
|
},
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
ui::Title {
|
|
text: name,
|
|
font: font.clone(),
|
|
},
|
|
))
|
|
.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(1.0)),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
..default()
|
|
},
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
ui::Title {
|
|
text: 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 destroy_entity_button(
|
|
current: &Query<(Entity, &ui::TargetEntity)>,
|
|
commands: &mut Commands,
|
|
target: &ui::TargetEntity,
|
|
) {
|
|
if let Some(entity) = current.iter().find_map(|(entity, this)| {
|
|
if this.entity == target.entity {
|
|
Some(entity)
|
|
} else {
|
|
None
|
|
}
|
|
}) {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
}
|
|
|
|
use gltf::*;
|
|
mod gltf {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct GltfWidget;
|
|
|
|
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().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()),
|
|
None,
|
|
);
|
|
}
|
|
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()),
|
|
None,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn control_active_gltf(
|
|
events: Query<Entity, (With<ui::TargetAsset<Gltf>>, Added<ui::Active>)>,
|
|
root: Query<Entity, With<LevelRoot>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.iter().for_each(|_| {
|
|
commands.entity(root.single()).despawn_descendants();
|
|
});
|
|
}
|
|
}
|
|
|
|
use scenes::*;
|
|
mod scenes {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct SceneWidget;
|
|
|
|
pub fn add_scenes_ui(
|
|
gltf_selected: Query<&ui::TargetAsset<Gltf>, Added<ui::Active>>,
|
|
mut commands: Commands,
|
|
gltfs: Res<Assets<Gltf>>,
|
|
widget: Query<Entity, With<SceneWidget>>,
|
|
) {
|
|
gltf_selected.iter().for_each(|ui::TargetAsset { handle }| {
|
|
if let Some(gltf) = gltfs.get(&handle.clone()) {
|
|
gltf.named_scenes.iter().for_each(|(name, handle)| {
|
|
create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
name.clone(),
|
|
None,
|
|
);
|
|
})
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn remove_scenes_ui(
|
|
mut gltf_unselected: RemovedComponents<ui::Active>,
|
|
target_assets: Query<&ui::TargetAsset<Gltf>>,
|
|
current: Query<(Entity, &ui::TargetAsset<Scene>)>,
|
|
gltfs: Res<Assets<Gltf>>,
|
|
mut commands: Commands,
|
|
) {
|
|
gltf_unselected
|
|
.iter()
|
|
.filter_map(|entity| target_assets.get(entity).ok())
|
|
.filter_map(|ui::TargetAsset { handle }| gltfs.get(handle))
|
|
.for_each(|gltf| {
|
|
gltf.scenes.iter().for_each(|handle| {
|
|
destroy_asset_button(
|
|
¤t,
|
|
&mut commands,
|
|
&ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn control_active_scenes(
|
|
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
mut removed: RemovedComponents<ui::Active>,
|
|
scene_refs: Query<&ui::TargetAsset<Scene>>,
|
|
scenes: Query<(Entity, &Handle<Scene>)>,
|
|
level_root: Query<Entity, With<LevelRoot>>,
|
|
mut commands: Commands,
|
|
) {
|
|
// A scene button was marked inactive
|
|
removed.iter().for_each(|entity| {
|
|
// Get the handle associated with that button
|
|
if let Ok(ui::TargetAsset { handle }) = scene_refs.get(entity) {
|
|
if let Some(entity) = scenes.iter().find_map(|(entity, this_handle)| {
|
|
if this_handle == handle {
|
|
Some(entity)
|
|
} else {
|
|
None
|
|
}
|
|
}) {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
});
|
|
added.iter().for_each(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = scene_refs.get(entity) {
|
|
info!("Spawning Scene {:?}", handle);
|
|
commands
|
|
.entity(level_root.single())
|
|
.with_children(|parent| {
|
|
parent.spawn(SceneBundle {
|
|
scene: handle.clone(),
|
|
..default()
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
use animations::*;
|
|
mod animations {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct AnimationWidget;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct AnimationPlayAll;
|
|
|
|
pub fn init_animations_ui(
|
|
events: Query<Entity, Added<AnimationWidget>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.iter().for_each(|entity| {
|
|
commands.entity(entity).with_children(|parent| {
|
|
parent.spawn((
|
|
AnimationPlayAll,
|
|
ButtonBundle {
|
|
style: Style {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
..default()
|
|
},
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
ui::Title {
|
|
text: "Play All".into(),
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
})
|
|
}
|
|
|
|
/// When a new scene is loaded, add any newly compatible animations
|
|
/// TODO: Add target entity(s) too
|
|
pub fn add_animations_ui(
|
|
player_spawned: Query<&Name, Added<AnimationPlayer>>,
|
|
widget: Query<Entity, With<AnimationWidget>>,
|
|
mut commands: Commands,
|
|
gltfs: Res<Assets<Gltf>>,
|
|
clips: Res<Assets<AnimationClip>>,
|
|
) {
|
|
player_spawned.iter().for_each(|player_name| {
|
|
gltfs
|
|
.iter()
|
|
.flat_map(|(_, gltf)| gltf.named_animations.iter())
|
|
.filter_map(|(clip_name, handle)| {
|
|
if let Some(clip) = clips.get(&handle) {
|
|
Some((clip_name, handle, clip))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.filter(|(_, _, clip)| clip.compatible_with(player_name))
|
|
.for_each(|(clip_name, handle, _)| {
|
|
create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
clip_name.clone(),
|
|
None,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
// When a scene is de-selected, remove any outdated animation options
|
|
pub fn remove_animations_ui(
|
|
mut removed_players: RemovedComponents<Handle<Scene>>,
|
|
current: Query<(Entity, &ui::TargetAsset<AnimationClip>)>,
|
|
clips: Res<Assets<AnimationClip>>,
|
|
targets: Query<(&AnimationPlayer, &Name)>,
|
|
mut commands: Commands,
|
|
) {
|
|
// For each removed scene
|
|
removed_players.iter().for_each(|_| {
|
|
// Iterate over the current animation buttons
|
|
current
|
|
.iter()
|
|
.filter(|(_, ui::TargetAsset { handle })| {
|
|
// Check if this clip is compatible with any remaining entities
|
|
// NOTE: We are checking this is *not* compatible with any entities
|
|
clips
|
|
.get(handle)
|
|
.map(|clip| !(targets.iter().any(|(_, name)| clip.compatible_with(name))))
|
|
.unwrap_or(true)
|
|
})
|
|
.for_each(|(_, ui::TargetAsset { handle })| {
|
|
// Destroy the buton if it is so
|
|
destroy_asset_button(
|
|
¤t,
|
|
&mut commands,
|
|
&ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn play_all_animations(
|
|
start: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
mut stop: RemovedComponents<ui::Active>,
|
|
play_all_btn: Query<Entity, With<AnimationPlayAll>>,
|
|
clip_btns: Query<Entity, With<ui::TargetAsset<AnimationClip>>>,
|
|
mut commands: Commands,
|
|
) {
|
|
stop.iter()
|
|
.filter(|&entity| play_all_btn.contains(entity))
|
|
.for_each(|_| {
|
|
clip_btns.iter().for_each(|entity| {
|
|
commands.entity(entity).remove::<ui::Active>();
|
|
})
|
|
});
|
|
start
|
|
.iter()
|
|
.filter(|&entity| play_all_btn.contains(entity))
|
|
.for_each(|_| {
|
|
clip_btns.iter().for_each(|entity| {
|
|
commands.entity(entity).insert(ui::Active);
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn play_animation(
|
|
start: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
mut stop: RemovedComponents<ui::Active>,
|
|
clip_refs: Query<&ui::TargetAsset<AnimationClip>>,
|
|
mut targets: Query<(&mut AnimationPlayer, &Name), With<Transform>>,
|
|
clips: Res<Assets<AnimationClip>>,
|
|
) {
|
|
stop.iter().for_each(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) {
|
|
let clip = clips.get(&handle).expect("Load animation clip");
|
|
targets
|
|
.iter_mut()
|
|
.filter(|(_, name)| clip.compatible_with(name))
|
|
.for_each(|(mut player, _)| {
|
|
player.pause();
|
|
})
|
|
}
|
|
});
|
|
start.iter().for_each(|entity| {
|
|
if let Ok(ui::TargetAsset { handle }) = clip_refs.get(entity) {
|
|
let clip = clips.get(&handle).expect("Load animation clip");
|
|
targets
|
|
.iter_mut()
|
|
.filter(|(_, name)| clip.compatible_with(name))
|
|
.for_each(|(mut player, _)| {
|
|
if player.is_paused() {
|
|
player.resume();
|
|
} else {
|
|
player.play(handle.clone()).repeat();
|
|
}
|
|
})
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
use fonts::*;
|
|
mod fonts {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct FontWidget;
|
|
|
|
#[derive(Debug, Resource, Default)]
|
|
pub struct FontInfo {
|
|
pub default: Option<Handle<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().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()),
|
|
Some(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()),
|
|
Some(handle.clone()),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn set_active_font(
|
|
events: Query<&ui::TargetAsset<Font>, Added<ui::Active>>,
|
|
mut font: ResMut<FontInfo>,
|
|
) {
|
|
events
|
|
.iter()
|
|
.for_each(|ui::TargetAsset { handle }| font.default = Some(handle.clone()));
|
|
}
|
|
}
|
|
|
|
use monologues::*;
|
|
mod monologues {
|
|
use super::*;
|
|
use bevy::{
|
|
reflect::{TypePath, TypeUuid},
|
|
ui::FocusPolicy,
|
|
};
|
|
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(Debug, Component)]
|
|
pub struct MonologueModal;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct MonologueContainer;
|
|
|
|
#[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 {
|
|
let asset = Monologue {
|
|
text: String::from_utf8(bytes.to_vec())?,
|
|
};
|
|
load_context.set_default_asset(LoadedAsset::new(asset));
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn extensions(&self) -> &[&str] {
|
|
&["monologue.txt"]
|
|
}
|
|
}
|
|
|
|
pub fn init_texts_ui(mut commands: Commands) {
|
|
commands.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(100.0),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
focus_policy: FocusPolicy::Pass,
|
|
..default()
|
|
},
|
|
MonologueContainer,
|
|
));
|
|
}
|
|
|
|
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| match event {
|
|
AssetEvent::Created { handle } => {
|
|
info!("Monologue created! {:?}", event);
|
|
create_asset_button(
|
|
&widget,
|
|
&mut commands,
|
|
ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
get_asset_name(&server, handle.clone()),
|
|
None,
|
|
);
|
|
}
|
|
AssetEvent::Removed { handle } => {
|
|
info!("Monologue removed! {:?}", event);
|
|
destroy_asset_button(
|
|
¤t,
|
|
&mut commands,
|
|
&ui::TargetAsset {
|
|
handle: handle.clone(),
|
|
},
|
|
);
|
|
}
|
|
AssetEvent::Modified { handle } => {
|
|
info!("Monologue 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()),
|
|
None,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn show_preview_text(
|
|
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
|
|
monologue_handles: Query<&ui::TargetAsset<Monologue>>,
|
|
monologues: Res<Assets<Monologue>>,
|
|
container: Query<Entity, With<MonologueContainer>>,
|
|
mut commands: Commands,
|
|
font: Res<FontInfo>,
|
|
) {
|
|
added
|
|
.iter()
|
|
.filter_map(|entity| monologue_handles.get(entity).ok())
|
|
.for_each(|ui::TargetAsset { handle }| {
|
|
let monologue = monologues.get(handle).expect("Preview loaded monologue");
|
|
commands
|
|
.entity(container.single())
|
|
.despawn_descendants()
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
max_width: Val::Percent(50.0),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
flex_direction: FlexDirection::Column,
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
ui::TitleBarBase::new(Color::VIOLET).bundle(),
|
|
ui::Title {
|
|
text: "Monologue".into(),
|
|
..default()
|
|
},
|
|
ui::Close {
|
|
target: parent.parent_entity(),
|
|
},
|
|
ui::Sorting(0),
|
|
));
|
|
let style = match &font.default {
|
|
Some(handle) => TextStyle {
|
|
color: Color::BLACK.into(),
|
|
font_size: 16.0,
|
|
font: handle.clone(),
|
|
..default()
|
|
},
|
|
None => TextStyle {
|
|
color: Color::BLACK.into(),
|
|
font_size: 16.0,
|
|
..default()
|
|
},
|
|
};
|
|
parent.spawn((
|
|
TextBundle::from_section(monologue.text.clone(), style),
|
|
handle.clone(),
|
|
));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// TODO: Sync Handle<Monologue> and TextStyle components to automagically generate and sync text
|
|
pub fn sync_monologue_font(
|
|
mut texts: Query<&mut Text, With<Handle<Monologue>>>,
|
|
font: Res<FontInfo>,
|
|
) {
|
|
if font.is_changed() || font.is_added() {
|
|
texts.iter_mut().for_each(|mut text| {
|
|
text.sections
|
|
.iter_mut()
|
|
.for_each(|section| match &font.default {
|
|
Some(handle) => section.style.font = handle.clone(),
|
|
None => section.style.font = Handle::default(),
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
use cameras::*;
|
|
mod cameras {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct CameraWidget;
|
|
|
|
pub fn cameras_ui(
|
|
mut added: Query<(Entity, &mut Camera, &Name), Added<Camera>>,
|
|
mut removed: RemovedComponents<Camera>,
|
|
editor_camera: Query<Entity, With<EditorCamera>>,
|
|
widget: Query<Entity, With<CameraWidget>>,
|
|
current: Query<(Entity, &ui::TargetEntity)>,
|
|
mut commands: Commands,
|
|
) {
|
|
removed.iter().for_each(|entity| {
|
|
info!("Destroy button for {:?}", entity);
|
|
destroy_entity_button(¤t, &mut commands, &ui::TargetEntity { entity });
|
|
});
|
|
added.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 = entity == editor_camera.single();
|
|
});
|
|
}
|
|
|
|
/// Set the camera active component based on button clicks
|
|
pub fn manage_active_camera(
|
|
events: Query<&ui::TargetEntity, Added<ui::Active>>,
|
|
mut cameras: Query<(Entity, &mut Camera)>,
|
|
) {
|
|
events.iter().for_each(|ui::TargetEntity { entity }| {
|
|
cameras.iter_mut().for_each(|(this_entity, mut camera)| {
|
|
if this_entity == *entity {
|
|
info!("Marking {:?} as active camera", entity);
|
|
camera.is_active = true;
|
|
} else {
|
|
info!("Marking {:?} as inactive camera", entity);
|
|
camera.is_active = false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// In the event that an active camera is despawned, fall back to the editor camera
|
|
pub fn fallback_camera(
|
|
modified: Query<Entity, (Changed<Camera>, Without<EditorCamera>)>,
|
|
mut removed: RemovedComponents<Camera>,
|
|
other_cameras: Query<&Camera, Without<EditorCamera>>,
|
|
mut editor_camera: Query<&mut Camera, With<EditorCamera>>,
|
|
) {
|
|
// Any time a camera is modified
|
|
modified.iter().chain(removed.iter()).for_each(|_| {
|
|
// If no other cameras are active
|
|
if !other_cameras.iter().any(|camera| camera.is_active) {
|
|
// Make the editor camera active
|
|
editor_camera.single_mut().is_active = true;
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
use lighting::*;
|
|
mod lighting {
|
|
use super::*;
|
|
|
|
pub fn spot_light_force_shadows(mut spot_lights: Query<&mut SpotLight, Added<SpotLight>>) {
|
|
spot_lights.iter_mut().for_each(|mut light| {
|
|
light.shadows_enabled = true;
|
|
})
|
|
}
|
|
|
|
pub fn directional_light_force_shadows(
|
|
mut directional_lights: Query<&mut DirectionalLight, Added<DirectionalLight>>,
|
|
) {
|
|
directional_lights.iter_mut().for_each(|mut light| {
|
|
light.shadows_enabled = true;
|
|
})
|
|
}
|
|
|
|
pub fn point_light_force_shadows(mut point_lights: Query<&mut PointLight, Added<PointLight>>) {
|
|
point_lights.iter_mut().for_each(|mut light| {
|
|
light.shadows_enabled = true;
|
|
})
|
|
}
|
|
}
|
|
|
|
use reset::*;
|
|
mod reset {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct ClearLevel;
|
|
|
|
pub fn clear_level(
|
|
events: Query<Entity, (With<ClearLevel>, Added<ui::Active>)>,
|
|
actives: Query<
|
|
Entity,
|
|
(
|
|
With<ui::Active>,
|
|
Or<(
|
|
With<ui::TargetEntity>,
|
|
With<ui::TargetAsset<Gltf>>,
|
|
With<ui::TargetAsset<Scene>>,
|
|
With<ui::TargetAsset<AnimationClip>>,
|
|
With<ui::TargetAsset<AudioSource>>,
|
|
With<ui::TargetAsset<Monologue>>,
|
|
With<ui::TargetAsset<Font>>,
|
|
)>,
|
|
),
|
|
>,
|
|
root: Query<Entity, With<LevelRoot>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.iter().for_each(|_| {
|
|
actives.iter().for_each(|entity| {
|
|
commands.entity(entity).remove::<ui::Active>();
|
|
});
|
|
commands.entity(root.single()).despawn_descendants();
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct ClearAssets;
|
|
|
|
pub fn clear_assets(
|
|
asset_holders: Query<
|
|
Entity,
|
|
Or<(
|
|
With<ui::TargetAsset<Gltf>>,
|
|
With<Handle<Gltf>>,
|
|
With<ui::TargetAsset<Scene>>,
|
|
With<Handle<Scene>>,
|
|
With<ui::TargetAsset<AnimationClip>>,
|
|
With<Handle<AnimationClip>>,
|
|
With<ui::TargetAsset<AudioSource>>,
|
|
With<Handle<AudioSource>>,
|
|
With<ui::TargetAsset<Monologue>>,
|
|
With<Handle<Monologue>>,
|
|
With<ui::TargetAsset<Font>>,
|
|
With<Handle<Font>>,
|
|
)>,
|
|
>,
|
|
mut registry: ResMut<AssetRegistry>,
|
|
mut commands: Commands,
|
|
) {
|
|
info!("Clearing assets");
|
|
|
|
// Clear buttons holding asset references
|
|
asset_holders
|
|
.iter()
|
|
.for_each(|entity| commands.entity(entity).despawn_recursive());
|
|
|
|
// Empty asset registry
|
|
registry.0.clear();
|
|
}
|
|
}
|