Compare commits

...

7 Commits

960
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -39,10 +39,14 @@ path = "bin/editor.rs"
name = "chars"
path = "bin/chars.rs"
[[bin]]
name = "serialize-wtf"
path = "bin/serialize-wtf.rs"
[dependencies]
bevy = "0.11"
bevy_rapier3d = "*"
# bevy_rapier3d = "*"
serde = "1"
# From rapier docs

BIN
assets/models/FlightHelmet.bin (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/FlightHelmet.gltf (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/materials.blend (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/materials.blend1 (Stored with Git LFS)

Binary file not shown.

@ -0,0 +1,100 @@
(
resources: {},
entities: {
1: (
components: {
"bevy_render::view::visibility::Visibility": Inherited,
"bevy_transform::components::transform::Transform": (
translation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_transform::components::global_transform::GlobalTransform": ((
matrix3: (
x_axis: (
x: 1.0,
y: 0.0,
z: 0.0,
),
y_axis: (
x: 0.0,
y: 1.0,
z: 0.0,
),
z_axis: (
x: 0.0,
y: 0.0,
z: 1.0,
),
),
translation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
)),
"editor::LevelRoot": (),
"bevy_hierarchy::components::children::Children": ([
198,
]),
},
),
198: (
components: {
"bevy_render::view::visibility::Visibility": Inherited,
"bevy_transform::components::transform::Transform": (
translation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_transform::components::global_transform::GlobalTransform": ((
matrix3: (
x_axis: (
x: 1.0,
y: 0.0,
z: 0.0,
),
y_axis: (
x: 0.0,
y: 1.0,
z: 0.0,
),
z_axis: (
x: 0.0,
y: 0.0,
z: 1.0,
),
),
translation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
)),
"bevy_hierarchy::components::parent::Parent": (1),
"bevy_hierarchy::components::children::Children": ([
199,
]),
"bevy_asset::handle::Handle<bevy_scene::scene::Scene>": (
id: AssetPathId(((13241355290950327508), (2370051114748836591))),
),
},
),
},
)

@ -3,30 +3,15 @@
// Editor for creating Monologue Trees levels
//
// BUGS:
// * Camera order ambiguity
// * Load new GLTF -> Despawn all level entities
//
// TODO:
// * Disable auto start/load
// * Better handle hide/close monologue
// * (hard) Harden Active Camera
// * (medium) Despawn entire scene when GLTF changed?
// * (medium) Select Font -> "Default Font" Resource
// * (medium) Pre-compute animation target entities
// * (medium) Animation buttons only visible when playable
// * (easy) Clear button to wipe spawned scene
// * edit textbox with actions
// * (brutal) export level
// * (hard) import level
//
// Asset types:
// * Audios (done)
// * Loop individual (done)
// * Gltfs (doing)
// * Scenes
// * Animations
// * Play/Pause all
// * Fonts (done)
// * Monologues (done)
// * (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},
@ -39,9 +24,8 @@ 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!(
"Import assets by dragging and dropping files into the editor\n",
"\n",
"Supported file types (for now):\n",
"* 3D: .gltf, .glb\n",
"* Audio: .ogg\n",
@ -67,13 +51,15 @@ fn main() {
..default()
}),
DebugInfoPlugin,
ui::GameUiPlugin,
ui::GameUiPlugin {
enable_alerts: true,
},
))
.register_type::<LevelRoot>()
.init_resource::<AssetRegistry>()
.init_resource::<FontInfo>()
.add_asset::<Monologue>()
.init_asset_loader::<MonologueLoader>()
.add_event::<CustomAssetEvent<Scene>>()
.add_event::<CustomAssetEvent<AnimationClip>>()
.add_systems(Startup, (initialize_ui, init_texts_ui, welcome_message))
.add_systems(
Update,
@ -89,26 +75,25 @@ fn main() {
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,
(
cameras_ui,
manage_active_camera,
control_active_camera,
fallback_camera,
gltf_ui,
texts_ui,
control_active_gltf,
show_preview_text,
sync_monologue_font,
),
)
.add_systems(Update, (audio_ui, play_audio))
.add_systems(Update, (fonts_ui, set_active_font))
.add_systems(Startup, reload_assets)
.add_systems(
Update,
(
import_files,
gltf_ui,
fonts_ui,
texts_ui,
manage_active_gltf,
show_preview_text,
sync_monologue_font,
reload_assets.run_if(ui::activated::<ReloadAssets>),
clear_assets.run_if(ui::activated::<ClearAssets>),
),
)
.add_systems(
@ -119,23 +104,27 @@ fn main() {
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,
),
)
.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)]
#[derive(Debug, Component, Reflect, Default)]
#[reflect(Component)]
pub struct LevelRoot;
#[derive(Debug, Component)]
@ -143,7 +132,7 @@ pub struct EditorCamera;
fn initialize_ui(mut commands: Commands) {
// Empty entity for populating the level being edited
commands.spawn((SpatialBundle { ..default() }, LevelRoot));
commands.spawn((SpatialBundle { ..default() }, LevelRoot::default()));
commands.spawn((
Camera2dBundle { ..default() },
@ -161,6 +150,15 @@ fn initialize_ui(mut commands: Commands) {
..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 {
@ -232,6 +230,11 @@ fn initialize_ui(mut commands: Commands) {
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,
@ -274,24 +277,19 @@ fn initialize_ui(mut commands: Commands) {
ui::Select::Single,
))
.with_children(|parent| {
let b = ButtonBundle {
style: Style {
..base_style.clone()
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),
));
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
};
content_containers.iter().for_each(|(name, target)| {
parent.spawn((
b.clone(),
ui::Title {
text: name.clone(),
..default()
},
ui::Collapse { target: *target },
));
});
);
});
})
.id();
@ -305,6 +303,91 @@ fn initialize_ui(mut commands: Commands) {
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: "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>) {
@ -345,27 +428,6 @@ fn spawn_tab_container<T: Default + Component>(
)
}
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 {
@ -442,29 +504,41 @@ mod audio {
});
}
pub fn play_audio(
events: Query<(Entity, &Interaction, &AudioSink), (With<Button>, Changed<Interaction>)>,
mut commands: Commands,
) {
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(|(_, &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);
}
});
.filter_map(|entity| sinks.get(entity).ok())
.for_each(|sink| sink.stop());
}
}
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() {
@ -578,7 +652,6 @@ mod gltf {
#[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,
@ -631,28 +704,17 @@ mod gltf {
});
}
pub fn manage_active_gltf(
events: Query<
(Entity, &Interaction, Option<&ui::Active>),
(With<Button>, Changed<Interaction>),
>,
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()
.filter(|(_, &interaction, _)| interaction == Interaction::Pressed)
.for_each(|(entity, _, active_ish)| match active_ish {
Some(_) => {
commands.entity(entity).remove::<ui::Active>();
}
None => {
commands.entity(entity).insert(ui::Active);
}
});
events.iter().for_each(|_| {
commands.entity(root.single()).despawn_descendants();
});
}
}
// TODO: Mark loaded animation as active
use scenes::*;
mod scenes {
use super::*;
@ -746,8 +808,6 @@ mod scenes {
}
}
// TODO: Play all animations
// TODO: Mark playing animation as active
use animations::*;
mod animations {
use super::*;
@ -786,6 +846,7 @@ mod animations {
}
/// 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>>,
@ -822,7 +883,6 @@ mod animations {
// When a scene is de-selected, remove any outdated animation options
pub fn remove_animations_ui(
mut removed_players: RemovedComponents<Handle<Scene>>,
names: Query<&Name>,
current: Query<(Entity, &ui::TargetAsset<AnimationClip>)>,
clips: Res<Assets<AnimationClip>>,
targets: Query<(&AnimationPlayer, &Name)>,
@ -921,6 +981,11 @@ mod fonts {
#[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,
@ -972,6 +1037,15 @@ mod fonts {
}
});
}
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::*;
@ -1095,6 +1169,7 @@ mod monologues {
monologues: Res<Assets<Monologue>>,
container: Query<Entity, With<MonologueContainer>>,
mut commands: Commands,
font: Res<FontInfo>,
) {
added
.iter()
@ -1131,15 +1206,21 @@ mod monologues {
},
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(),
TextStyle {
color: Color::BLACK.into(),
font_size: 16.0,
..default()
},
),
TextBundle::from_section(monologue.text.clone(), style),
handle.clone(),
));
});
@ -1149,16 +1230,19 @@ mod monologues {
// TODO: Sync Handle<Monologue> and TextStyle components to automagically generate and sync text
pub fn sync_monologue_font(
events: Query<&ui::TargetAsset<Font>, Added<ui::Active>>,
mut texts: Query<&mut Text, With<Handle<Monologue>>>,
font: Res<FontInfo>,
) {
events.iter().for_each(|ui::TargetAsset { handle }| {
if font.is_changed() || font.is_added() {
texts.iter_mut().for_each(|mut text| {
text.sections.iter_mut().for_each(|section| {
section.style.font = handle.clone();
});
text.sections
.iter_mut()
.for_each(|section| match &font.default {
Some(handle) => section.style.font = handle.clone(),
None => section.style.font = Handle::default(),
});
});
});
}
}
}
@ -1170,8 +1254,9 @@ mod cameras {
pub struct CameraWidget;
pub fn cameras_ui(
mut added: Query<(Entity, &mut Camera, &Name), (Added<Camera>, Without<EditorCamera>)>,
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,
@ -1188,47 +1273,25 @@ mod cameras {
ui::TargetEntity { entity },
name.as_str().into(),
);
camera.is_active = false;
camera.is_active = entity == editor_camera.single();
});
}
/// Set the camera active component based on button clicks
pub fn manage_active_camera(
events: Query<(&Interaction, &ui::TargetEntity), Changed<Interaction>>,
cameras: Query<Entity, With<Camera>>,
mut commands: Commands,
events: Query<&ui::TargetEntity, Added<ui::Active>>,
mut cameras: Query<(Entity, &mut Camera)>,
) {
events
.iter()
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
.for_each(|(_, ui::TargetEntity { entity })| {
cameras.iter().for_each(|this_entity| {
if this_entity == *entity {
info!("Marking {:?} as active camera", entity);
commands.entity(this_entity).insert(ui::Active);
} else {
info!("Marking {:?} as inactive camera", entity);
commands.entity(this_entity).remove::<ui::Active>();
}
});
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;
}
});
}
/// Set the active camera based on the Active marker component
pub fn control_active_camera(
added: Query<Entity, (Added<ui::Active>, With<Camera>)>,
mut removed: RemovedComponents<ui::Active>,
mut cameras: Query<&mut Camera>,
) {
removed.iter().for_each(|entity| {
if let Ok(mut camera) = cameras.get_mut(entity) {
camera.is_active = false;
}
});
added.iter().for_each(|entity| {
if let Ok(mut camera) = cameras.get_mut(entity) {
camera.is_active = true;
}
});
}
@ -1274,3 +1337,226 @@ mod lighting {
})
}
}
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();
}
}
pub use level::*;
mod level {
use std::fs::File;
use std::io::Write;
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 }| {
commands.entity(root.single()).despawn_recursive();
commands.spawn(DynamicSceneBundle {
scene: handle.clone(),
..default()
});
});
}
pub fn export_level(
root: Query<Entity, With<LevelRoot>>,
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();
// builder.allow_all();
builder.deny::<ComputedVisibility>();
// Level administrivia
builder.allow::<LevelRoot>();
// Scene components
builder.allow::<Handle<Scene>>();
builder.allow::<Visibility>();
builder.allow::<Transform>();
builder.allow::<GlobalTransform>();
// Audio components
builder.allow::<Handle<AudioSource>>();
builder.allow::<PlaybackSettings>();
root.iter().for_each(|r| {
// Extract the level root
builder.extract_entity(r);
match children.get(r) {
Ok(cs) => {
builder.extract_entities(cs.iter().map(|&entity| entity));
}
Err(e) => warn!("Empty level! {:?}", e),
}
});
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
File::create(format!("assets/output.scn.ron"))
.and_then(|mut file| file.write(serialized.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}
pub fn rehydrate_level(
events: Query<Entity, (Added<Visibility>, Without<ComputedVisibility>)>,
mut commands: Commands,
) {
events.iter().for_each(|entity| {
commands
.entity(entity)
.insert(ComputedVisibility::default());
});
}
}

@ -0,0 +1,105 @@
// TODO:
// * Determine where local assets folder needs to be created
// * Create local assets folder
// * Recusrively laod that folder and watch for changes
// * Copy assets into local folder in leue of importing them
// * Check portability by moving binary + folder to new location
use std::time::Duration;
use bevy::{asset::ChangeWatcher, prelude::*};
use monologue_trees::{debug::*, ui};
fn main() {
App::new()
.add_plugins((
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "Serialization WTF".into(),
resolution: (640., 480.).into(),
..default()
}),
..default()
})
.set(AssetPlugin {
asset_folder: "assets".into(),
// Tell the asset server to watch for asset changes on disk:
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(0)),
..default()
}),
DebugInfoPlugin,
ui::GameUiPlugin { ..default() },
))
.register_type::<LevelRoot>()
.register_type::<Other>()
.register_type::<Choice>()
.add_systems(Startup, init)
.add_systems(Update, serialize_debug.run_if(pending))
.run();
}
#[derive(Debug, Component, Default, Reflect)]
#[reflect(Component)]
struct LevelRoot;
#[derive(Debug, Component, Default, Reflect)]
#[reflect(Component)]
struct Other {
inner: String,
}
#[derive(Debug, Component, Default, Reflect)]
#[reflect(Component)]
enum Choice {
#[reflect(default)]
#[default]
One,
Two,
Three,
}
fn init(mut commands: Commands) {
commands.spawn((
SpatialBundle { ..default() },
LevelRoot::default(),
Name::new("Root Entity"),
Other::default(),
Choice::default(),
));
}
fn serialize_debug(query: Query<Entity, With<LevelRoot>>, world: &World) {
let entities = query.iter().collect();
print!("{}", ser(entities, world));
}
fn pending(query: Query<Entity, With<LevelRoot>>, mut done: Local<bool>) -> bool {
if !*done {
if query.iter().len() > 0 {
*done = true;
} else {
*done = false;
}
return *done;
}
return false;
}
fn ser(entities: Vec<Entity>, world: &World) -> String {
let app_type_registry = world.resource::<AppTypeRegistry>().clone();
let scene = {
let mut builder = DynamicSceneBuilder::from_world(world.clone());
builder
.allow_all_resources()
.allow_all()
.deny::<ComputedVisibility>()
.extract_entities(entities.into_iter());
builder.build()
};
scene
.serialize_ron(&app_type_registry)
.expect("Serialize scene")
}

@ -1,9 +1,5 @@
/// TODO:
/// * Text box w/ "reset" button
/// * Generic Minimize/Close Components
/// * Title bar w/ min/close controls
/// * Notice/Warning/Error Popups
/// * Textbox (ReadOnly)
/// * Textbox (ReadWrite)
/// * Move code to submodules
///
@ -25,13 +21,15 @@ pub struct TargetEntity {
pub entity: Entity,
}
pub struct GameUiPlugin;
/// UI Plugin
#[derive(Default)]
pub struct GameUiPlugin {
pub enable_alerts: bool,
}
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut App) {
app.add_event::<Alert>()
.add_systems(Startup, init_alerts)
.add_systems(PreUpdate, create_titles)
app.add_systems(PreUpdate, create_titles)
.add_systems(
Update,
(
@ -44,10 +42,15 @@ impl Plugin for GameUiPlugin {
manage_collapse_active,
show_child_number,
manage_sort,
spawn_alert,
),
)
.add_systems(PostUpdate, close_window);
if self.enable_alerts {
app.add_event::<Alert>()
.add_systems(Startup, init_alerts)
.add_systems(Update, spawn_alert);
}
}
}
@ -249,14 +252,22 @@ mod title {
events.iter().for_each(|(title, note, children)| {
children.iter().for_each(|child| {
if let Ok(mut text) = texts.get_mut(*child) {
let style = match &title.font {
Some(handle) => TextStyle {
color: Color::BLACK,
font: handle.clone(),
..default()
},
None => TextStyle {
color: Color::BLACK,
..default()
},
};
*text = Text {
sections: vec![
TextSection {
value: title.text.clone(),
style: TextStyle {
color: Color::BLACK,
..default()
},
style: style,
},
TextSection {
value: note
@ -423,28 +434,63 @@ mod buttons {
pub enum Select {
Multi,
Single,
Action,
}
pub fn manage_select_active(
events: Query<(Entity, &Parent), Added<Active>>,
children: Query<(&Select, &Children)>,
events: Query<
(Entity, &Parent, Option<&Active>, &Interaction),
(With<Button>, Changed<Interaction>),
>,
selects: Query<(&Select, &Children)>,
mut commands: Commands,
) {
events.iter().for_each(|(entity, parent)| {
if let Ok((select, childs)) = children.get(parent.get()) {
match select {
Select::Single => {
childs
.iter()
.filter(|&child| *child != entity)
.for_each(|&child| {
commands.entity(child).remove::<Active>();
})
}
Select::Multi => (),
}
}
});
events
.iter()
.for_each(|(entity, parent, active, interaction)| {
selects
.get(parent.get())
.iter()
.for_each(|(select, children)| match interaction {
Interaction::Pressed => {
match active {
Some(_) => {
commands.entity(entity).remove::<Active>();
}
None => {
commands.entity(entity).insert(Active);
}
}
match select {
Select::Multi | Select::Action => (),
Select::Single => {
children.iter().filter(|&child| *child != entity).for_each(
|&child| {
commands.entity(child).remove::<Active>();
},
);
}
}
}
// A silly hack to get actions to maintain the active tag for 1+ frames
Interaction::None => match select {
Select::Action => {
commands.entity(entity).remove::<Active>();
}
_ => (),
},
_ => (),
});
});
}
/// run_if helper for simple button->trigger action
pub fn activated<T: Component>(events: Query<Entity, (With<T>, Added<Active>)>) -> bool {
events.iter().any(|_| true)
}
pub fn event<T: Event>(mut events: EventReader<T>) -> bool {
events.iter().len() > 0
}
}
@ -478,7 +524,7 @@ mod scroll {
self_hover || child_hover || parent_hover
})
.for_each(|(_, _, _, mut style)| {
let factor = 2.0;
let factor = 8.0;
let val = match unit {
MouseScrollUnit::Line => match style.top {
Val::Px(v) => v + (y * factor),
@ -562,8 +608,8 @@ pub mod alert {
pub struct AlertsWidget;
pub fn init_alerts(mut commands: Commands) {
commands.spawn((
NodeBundle {
commands
.spawn(NodeBundle {
style: Style {
top: Val::Px(0.0),
right: Val::Px(0.0),
@ -578,9 +624,35 @@ pub mod alert {
},
border_color: Color::WHITE.into(),
..default()
},
AlertsWidget,
));
})
.with_children(|parent| {
let container = parent
.spawn((
NodeBundle {
style: Style {
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,
justify_items: JustifyItems::Center,
..default()
},
border_color: Color::WHITE.into(),
..default()
},
AlertsWidget,
))
.id();
parent.spawn((
TitleBarBase::new(Color::WHITE).bundle(),
Title {
text: "Alerts".into(),
..default()
},
Minimize { target: container },
Sorting(0),
));
});
}
pub fn spawn_alert(

Loading…
Cancel
Save