use super::*; pub(crate) struct TreesDebugPlugin; impl Plugin for TreesDebugPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, init_debug_ui) .add_systems( Update, ( (spawn_debug_buttons.run_if(on_event::>),), ( monologue_asset_tooltip .run_if(on_event::>.or(on_event::>)), hide_menu.run_if(any_component_changed::), clear_monologue.run_if(any_component_changed::), control_menu.run_if(on_event::>.or(on_event::>)), delete_tree.run_if(on_event::>), drag_tree.run_if(on_event::>), ) .run_if(in_state(DebuggingState::On)), ), ) .add_observer(add_dialog_option); } } #[derive(Component)] struct MonologuesContainer; #[derive(Component)] struct MonologuesList; #[derive(Component)] struct MonologuePreview; fn drag_tree( mut events: EventReader>, state: Res>, mut query: Query<&mut Transform, With>, camera: Single<(&Camera, &GlobalTransform), With>, window: Single<&Window>, ) { debug_assert_eq!(*state.get(), DebuggingState::On); events.read().for_each(|event| { if let Ok(mut t) = query.get_mut(event.target) { let world_position = window .cursor_position() .and_then(|cursor| camera.0.viewport_to_world(camera.1, cursor).ok()) .map(|ray| { // Compute ray's distance to entity let distance = ray .intersect_plane(t.translation, InfinitePlane3d::new(t.up())) .unwrap(); ray.get_point(distance) }); t.translation = world_position.unwrap(); } }); } /// Panel for selecting which monologue tree to spawn fn init_debug_ui(mut commands: Commands) { let mut monologue_button = None; commands .spawn(( Node { top: Val::Px(0.0), left: Val::Px(0.0), flex_direction: FlexDirection::Row, ..default() }, DebuggingState::On, )) .with_children(|parent| { monologue_button = Some( parent .spawn(( Name::new("Monologue Assignment Menu"), children![Text::new("+Monologue"),], Button, Node { min_width: Val::Px(25.0), min_height: Val::Px(25.0), ..default() }, DebuggingState::On, MonologuesContainer, )) .id(), ); parent .spawn(( Name::new("Tree Planter"), children![Text::new("+Tree"),], Node { min_width: Val::Px(25.0), min_height: Val::Px(25.0), ..default() }, DebuggingState::On, MonologuesContainer, Button, )) .observe(spawn_tree); }); commands .spawn(( NavParent(monologue_button.unwrap()), NavState::default(), DebuggingState::On, Name::new("Container"), Node { flex_direction: FlexDirection::Row, top: Val::Px(25.0), height: Val::Percent(90.0), width: Val::Percent(80.0), ..default() }, )) .with_children(|parent| { parent .spawn(( Name::new("Buttons"), Node { flex_direction: FlexDirection::Column, height: Val::Percent(100.0), width: Val::Percent(40.0), overflow: Overflow::scroll_y(), ..default() }, ScrollPosition::default(), MonologuesList, )) .observe(scroll); parent.spawn(( Name::new("Preview"), MonologuePreview, NavParent(monologue_button.unwrap()), NavState::default(), Node { flex_direction: FlexDirection::Column, padding: UiRect::all(Val::Px(10.0)), overflow: Overflow::scroll_y(), width: Val::Percent(60.0), ..default() }, )); }); } // When you pointer goes off of the '+' or any of it's children make the entire menu invisible fn control_menu( mut over_events: EventReader>, mut out_events: EventReader>, nav_children: Query<&NavParent>, children: Query<&ChildOf>, nav_parents: Query<&NavChildren>, parents: Query<&Children>, mut nav: Query<&mut NavState>, hover_map: Res, ) { over_events.read().for_each(|over| { let root = nav_children.root_ancestor(over.target); nav_parents.iter_descendants(root).for_each(|child| { if let Ok(mut n) = nav.get_mut(child) { *n = NavState::Open; } }); }); // Gives us the enities covered by the HoverMap let is_hovered: Vec<&Entity> = hover_map .iter() .flat_map(|(_, submap)| { let x: Vec<&Entity> = submap.iter().map(|(node, _)| node).collect(); x }) .collect(); // For all pointer out events out_events.read().for_each(|out| { // If a relative of out.target is hovered, do nothing // Otherwise set to closed let root = children.root_ancestor(out.target); let tree_still_hovered = parents .iter_descendants(root) .any(|child| is_hovered.contains(&&child)); if !tree_still_hovered { if let Ok(mut n) = nav.get_mut(root) { *n = NavState::Closed; } parents.iter_descendants(root).for_each(|child| { if let Ok(mut n) = nav.get_mut(child) { *n = NavState::Closed; } }) } }) } /// When a (Text) dialog option is added: /// 1. Add the Button component /// 2. Change the color to Orange /// 3. Add observers for click (select) and hover (change color) fn add_dialog_option(trigger: Trigger, mut commands: Commands) { commands .entity(trigger.target()) .insert(Button) .insert(Node { width: Val::Percent(100.0), ..default() }) .insert(TextLayout::new_with_justify(JustifyText::Center)) .insert(TextColor(ORANGE.into())) .observe(choose_dialog_option) .observe(hover_dialog_option_over) .observe(hover_dialog_option_out); } fn assign_monologue_event( trigger: Trigger>, mut events: EventWriter, monologues: Query<&TreeMonologue>, ) { let TreeMonologue(handle) = monologues.get(trigger.target()).unwrap(); events.write(AssignMonologue(handle.clone())); } /// Observer for the "Plant a new tree" button in the debug UI fn spawn_tree(_trigger: Trigger>, mut events: EventWriter) { events.write(PlantTree(None)); } fn clear_monologue( mut nodes: Query< (Entity, &NavState), (Changed, With, Without), >, mut commands: Commands, ) { nodes.iter_mut().for_each(|(e, n)| { if matches!(n, NavState::Closed) { commands.entity(e).despawn_related::(); } }); } // When you pointer over the '+' make the entire menu visible fn hide_menu(mut nodes: Query<(&mut Visibility, &NavState), Changed>) { nodes.iter_mut().for_each(|(mut v, n)| { *v = match n { NavState::Open => Visibility::Inherited, NavState::Closed => Visibility::Hidden, }; }); } fn delete_tree( mut events: EventReader>, mut commands: Commands, query: Query>, ) { events.read().for_each(|event| { if matches!(event.event.button, PointerButton::Middle) && query.contains(event.target) { debug!("Middle Click -> Despawning {}", event.target); commands.entity(event.target).despawn(); } }) } /// Add the "script: path/to/file.mono" tooltip info fn monologue_asset_tooltip( mut over_events: EventReader>, mut out_events: EventReader>, mut tooltip: ResMut, trees: Query<(&Tree, Option<&TreeMonologue>)>, ) { out_events .read() .filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target)) .for_each(|_| { tooltip.remove("Script"); }); over_events .read() .filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target)) .for_each(|e| match trees.get(e) { Ok((_tree, Some(TreeMonologue(handle)))) => match handle.path() { Some(p) => tooltip.insert("Script", format!("{p}")), None => tooltip.insert("Script", "A".into()), }, Ok((_tree, None)) => { tooltip.insert("Script", "N/A".into()); } _ => (), }); } /// When a dialog option is chosen (clicked on) we do the following: fn choose_dialog_option( trigger: Trigger>, mut dialog_events: EventWriter, mut commands: Commands, texts: Query<&Text>, options: Query>, dialog_box: Single>, ) { debug!("Choosing dialog {:?}", trigger.target()); debug!("Despawning dialog options"); options.iter().for_each(|e| { commands.entity(e).despawn(); }); debug!("Inserting dialog line"); if let Ok(t) = texts.get(trigger.target()) { commands.entity(*dialog_box).with_children(|parent| { parent.spawn((t.clone(), DialogLine)); }); } // trigger the next dialog line dialog_events.write(DialogEvent::NextBatch); } fn preview_monologue( trigger: Trigger>, container: Single, Without