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.

1648 lines
55 KiB
Rust

// Monologe Trees Editor
//
// 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
// * (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::{Asset, AssetLoader, Assets, ChangeWatcher, LoadContext, LoadedAsset},
audio::PlaybackMode,
core_pipeline::tonemapping::DebandDither,
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,
},
))
.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,
(
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,
(
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>,
//rehydrate_level::<Handle<AnimationClip>, AnimationPlayer>,
//rehydrate_level::<DebandDither, Camera3d>,
),
)
.run();
}
#[derive(Resource, Default)]
pub struct AssetRegistry(Vec<HandleUntyped>);
#[derive(Debug, Component)]
pub struct TabRoot;
#[derive(Debug, Component, Reflect, Default)]
#[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));
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()
};
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::<LevelWidget>(
"Level",
parent,
ui::Select::Single,
));
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(),
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),
));
});
}
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);
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(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&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 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: false,
..default()
},
});
});
})
}
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
}
}) {
commands.entity(entity).despawn_recursive();
}
}
});
}
}
use assets::*;
mod assets {
use super::*;
#[derive(Debug, Component)]
pub struct ReloadAssets;
pub fn reload_assets(
server: Res<AssetServer>,
mut registry: ResMut<AssetRegistry>,
mut writer: EventWriter<ui::Alert>,
) {
match server.load_folder(".") {
Ok(handles) => registry.0 = handles,
Err(e) => writer.send(ui::Alert::Warn(format!(
"Could not find `assets` folder!\n{:?}",
e
))),
}
}
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(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&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(|_| {
root.iter().for_each(|entity| {
commands.entity(entity).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(
&current,
&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(
&current,
&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(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&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(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Monologue modified! {:?}", event);
destroy_asset_button(
&current,
&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(&current, &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>();
});
root.iter().for_each(|entity| {
commands.entity(entity).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();
}
}
pub use level::*;
mod level {
use bevy::tasks::IoTaskPool;
use super::*;
type Level = DynamicScene;
#[derive(Debug, Component, Default)]
pub struct LevelWidget;
#[derive(Debug, Component)]
pub struct ExportLevel;
pub fn level_ui(
mut events: EventReader<AssetEvent<Level>>,
mut commands: Commands,
widget: Query<Entity, With<LevelWidget>>,
current: Query<(Entity, &ui::TargetAsset<Level>)>,
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(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&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 load_level(
events: Query<
&ui::TargetAsset<DynamicScene>,
(Added<ui::Active>, With<ui::TargetAsset<DynamicScene>>),
>,
root: Query<Entity, With<LevelRoot>>,
mut commands: Commands,
) {
events.iter().for_each(|ui::TargetAsset { handle }| {
root.iter().for_each(|entity| {
commands.entity(entity).despawn_recursive();
});
commands.spawn(DynamicSceneBundle {
scene: handle.clone(),
..default()
});
});
}
pub fn export_level(
level_root: Query<Entity, With<LevelRoot>>,
audio_root: Query<Entity, With<AudioRoot>>,
children: Query<&Children>,
world: &World,
) {
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let mut builder = DynamicSceneBuilder::from_world(world.clone());
builder.deny_all_resources();
// Exclude computed visibility
builder.deny_all();
// Level administrivia
builder.allow::<LevelRoot>();
builder.allow::<AudioRoot>();
// Scene components
builder.allow::<Handle<Scene>>();
// Spatial components
builder.allow::<Transform>();
builder.allow::<GlobalTransform>();
builder.allow::<Visibility>();
// Audio components
builder.allow::<Handle<AudioSource>>();
builder.allow::<PlaybackSettings>();
// Text components
builder.allow::<Handle<Font>>();
builder.allow::<Handle<Monologue>>();
level_root.iter().for_each(|level| {
// Extract the level root
builder.extract_entity(level);
if let Ok(kids) = children.get(level) {
builder.extract_entities(kids.into_iter().map(|&e| e));
} else {
warn!("Level is empty!");
}
});
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!");
}
});
let scene = builder.build();
let serialized = scene
.serialize_ron(&app_type_registry)
.expect("Serialize scene");
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
std::fs::write(format!("assets/output.scn.ron"), serialized.as_bytes())
.expect("Error while writing scene to file");
})
.detach();
}
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(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);
}
}