diff --git a/rs/engine/src/mats/fog.rs b/rs/engine/src/mats/fog.rs index d7cd066..86a3b54 100644 --- a/rs/engine/src/mats/fog.rs +++ b/rs/engine/src/mats/fog.rs @@ -23,7 +23,6 @@ pub type Matter = ExtendedMaterial; pub const BAYER_HANDLE: Handle = Handle::weak_from_u128(92299220200241619468604683494190943784); - /// Function instead of a constant because it uses floating-point math. #[inline(always)] pub fn max_fog_distance() -> f32 { @@ -133,22 +132,23 @@ impl AsMut for DistanceDither { impl AsRef for ExtendedMaterial where B: Material, - E: MaterialExtension + AsRef { + E: MaterialExtension + AsRef, +{ fn as_ref(&self) -> &DistanceDither { self.extension.as_ref() } } impl AsMut for ExtendedMaterial - where - B: Material, - E: MaterialExtension + AsMut { +where + B: Material, + E: MaterialExtension + AsMut, +{ fn as_mut(&mut self) -> &mut DistanceDither { self.extension.as_mut() } } - impl Default for DistanceDither { fn default() -> Self { Self::new( diff --git a/rs/engine/src/ui.rs b/rs/engine/src/ui.rs index e773079..2d2b459 100644 --- a/rs/engine/src/ui.rs +++ b/rs/engine/src/ui.rs @@ -1,5 +1,5 @@ use crate::{ - anim::{AnimationPlugin, AnimationController, ComponentDelta, StartAnimation}, + anim::{AnimationController, AnimationPlugin, ComponentDelta, StartAnimation}, entity_tree, input::InputState, mats::{ @@ -27,6 +27,7 @@ use bevy::{ ecs::{ query::{QueryEntityError, QuerySingleError}, schedule::SystemConfigs, + system::EntityCommands, }, input::common_conditions::input_toggle_active, pbr::ExtendedMaterial, @@ -43,9 +44,11 @@ use leafwing_input_manager::{prelude::*, Actionlike}; use meshtext::{MeshGenerator, QualitySettings}; use rapier3d::{geometry::SharedShape, math::Point}; use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, f64::consts::TAU}; -use std::ops::{Add, Mul}; -use bevy::ecs::system::EntityCommands; +use std::{ + collections::VecDeque, + f64::consts::TAU, + ops::{Add, Mul}, +}; pub mod a11y; #[cfg(feature = "debugging")] @@ -82,50 +85,49 @@ impl Plugin for UiPlugin { }, ); - app - .add_plugins(( - InputManagerPlugin::::default(), - AnimationPlugin::::default(), - )) - .register_type::() - .register_type::() - .register_type::() - .init_resource::>() - .insert_resource(UiAction::default_mappings()) - .init_resource::() - .init_resource::() - .init_asset::() - .register_asset_loader(Font3dLoader) - .add_systems(Startup, setup) - .add_systems(Update, (reset_hovered, show_fps, focus::resolve_focus)) - .add_systems( - PostUpdate, - ( - layout::apply_constraints, - highlight_focus::, - widgets::InteractHandlers::system, - ), - ) - .add_systems( - Last, - ( - Prev::::update_component, - hide_orphaned_popups, - propagate_fade::.before(Fade::hide_faded_out), - Fade::hide_faded_out, - anchor_follow_menu, - CuboidPanel::::sync, - Text3d::sync, - ), - ) - .insert_gizmo_group( - focus::FocusGizmos::, - GizmoConfig { - line_width: 6.0, - render_layers: GLOBAL_UI_RENDER_LAYERS, - ..default() - }, - ); + app.add_plugins(( + InputManagerPlugin::::default(), + AnimationPlugin::::default(), + )) + .register_type::() + .register_type::() + .register_type::() + .init_resource::>() + .insert_resource(UiAction::default_mappings()) + .init_resource::() + .init_resource::() + .init_asset::() + .register_asset_loader(Font3dLoader) + .add_systems(Startup, setup) + .add_systems(Update, (reset_hovered, show_fps, focus::resolve_focus)) + .add_systems( + PostUpdate, + ( + layout::apply_constraints, + highlight_focus::, + widgets::InteractHandlers::system, + ), + ) + .add_systems( + Last, + ( + Prev::::update_component, + hide_orphaned_popups, + propagate_fade::.before(Fade::hide_faded_out), + Fade::hide_faded_out, + anchor_follow_menu, + CuboidPanel::::sync, + Text3d::sync, + ), + ) + .insert_gizmo_group( + focus::FocusGizmos::, + GizmoConfig { + line_width: 6.0, + render_layers: GLOBAL_UI_RENDER_LAYERS, + ..default() + }, + ); } fn finish(&self, app: &mut App) { @@ -336,6 +338,7 @@ impl UiAction { // KB & Mouse (Ok, Space.into()), (Ok, Backspace.into()), + (Ok, Enter.into()), (Back, Backspace.into()), (MoveCursor, VirtualDPad::wasd().into()), (MoveCursor, VirtualDPad::arrow_keys().into()), @@ -611,14 +614,16 @@ pub fn anchor_follow_menu( } } +use crate::{ + anim::{AnimationHandle, Delta, DynAnimation}, + ui::widgets::new_unlit_material, +}; #[cfg(feature = "debugging")] use bevy_inspector_egui::{ inspector_options::std_options::NumberDisplay::Slider, prelude::{InspectorOptions, ReflectInspectorOptions}, }; use web_time::Duration; -use crate::anim::{AnimationHandle, Delta, DynAnimation}; -use crate::ui::widgets::new_unlit_material; /// Component that starts a new branch of a tree of entities that can be /// faded in an out together. @@ -647,10 +652,8 @@ pub struct Fade( impl Fade { pub const ZERO: Self = Self(0.0); pub const ONE: Self = Self(1.0); - - pub fn hide_faded_out( - mut q: Query<(&Self, &mut Visibility), Changed>, - ) { + + pub fn hide_faded_out(mut q: Query<(&Self, &mut Visibility), Changed>) { for (fade, mut vis) in &mut q { let new = if fade.0 <= 0.0 { Visibility::Hidden @@ -672,7 +675,7 @@ impl Default for Fade { impl Diff for Fade { type Delta = Self; - + fn delta_from(&self, rhs: &Self) -> Self::Delta { Self(self.0 - rhs.0) } @@ -680,7 +683,7 @@ impl Diff for Fade { impl Mul for Fade { type Output = Self; - + fn mul(self, rhs: f32) -> Self::Output { Self(self.0 * rhs) } @@ -688,7 +691,7 @@ impl Mul for Fade { impl Add for Fade { type Output = Self; - + fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) } @@ -751,7 +754,7 @@ pub fn propagate_fade>( pub trait FadeCommands { fn fade_in(&mut self, duration: Duration) -> AnimationHandle>; fn fade_out(&mut self, duration: Duration) -> AnimationHandle>; - + fn fade_in_secs(&mut self, secs: f32) -> AnimationHandle> { self.fade_in(Duration::from_secs_f32(secs)) } @@ -776,7 +779,7 @@ impl FadeCommands for EntityCommands<'_> { ComponentDelta::diffable(id, t, Fade(t)) }) } - + fn fade_out(&mut self, duration: Duration) -> AnimationHandle> { let mut elapsed = Duration::ZERO; let duration = duration.as_secs_f32(); diff --git a/rs/engine/src/ui/layout.rs b/rs/engine/src/ui/layout.rs index 2d29a75..6f99875 100644 --- a/rs/engine/src/ui/layout.rs +++ b/rs/engine/src/ui/layout.rs @@ -37,7 +37,7 @@ impl LineUpChildren { } } - pub fn with_additional_spacing(self, spacing: f32) -> Self { + pub fn with_spacing(self, spacing: f32) -> Self { Self { relative_positions: self.relative_positions.normalize() * (1.0 + spacing), align: self.align, @@ -84,7 +84,7 @@ pub fn apply_constraints( 1 => { match transforms.get_mut(children[0]) { Ok(mut child) => child.1.translation = Vec3::ZERO, - Err(e) => cmds.debug_components(id, e), + Err(e) => cmds.debug_components(children[0], e), } continue; } @@ -95,7 +95,8 @@ pub fn apply_constraints( let [a, b] = match transforms.get_many([pair[0], pair[1]]) { Ok(pair) => pair, Err(e) => { - cmds.debug_components(id, e); + cmds.debug_components(pair[0], e); + cmds.debug_components(pair[1], e); continue; } }; @@ -116,7 +117,7 @@ pub fn apply_constraints( let mut first_child = match transforms.get_mut(children[0]) { Ok(child) => child, Err(e) => { - cmds.debug_components(id, e); + cmds.debug_components(children[0], e); continue; } }; @@ -125,7 +126,8 @@ pub fn apply_constraints( let [a, mut b] = match transforms.get_many_mut([pair[0], pair[1]]) { Ok(pair) => pair, Err(e) => { - cmds.debug_components(id, e); + cmds.debug_components(pair[0], e); + cmds.debug_components(pair[1], e); continue; } }; diff --git a/rs/engine/src/ui/widgets.rs b/rs/engine/src/ui/widgets.rs index b4baffd..51e2609 100644 --- a/rs/engine/src/ui/widgets.rs +++ b/rs/engine/src/ui/widgets.rs @@ -1,4 +1,5 @@ use crate::{ + mats::{fade::DitherFade, fog::DistanceDither}, todo_warn, ui::{a11y::AKNode, TextMeshCache, UiAction, UiCam, UiMat, GLOBAL_UI_RENDER_LAYERS}, util::Prev, @@ -6,6 +7,7 @@ use crate::{ use bevy::{ a11y::accesskit::{NodeBuilder, Role}, ecs::system::{EntityCommand, EntityCommands}, + pbr::ExtendedMaterial, prelude::*, render::{ mesh::{Indices, PrimitiveTopology::TriangleList}, @@ -24,10 +26,7 @@ use std::{ ops::ControlFlow, sync::Arc, }; -use bevy::pbr::ExtendedMaterial; use web_time::Duration; -use crate::mats::fade::DitherFade; -use crate::mats::fog::DistanceDither; #[derive(Component, Clone, Deref, DerefMut)] pub struct WidgetShape(pub SharedShape); @@ -424,6 +423,7 @@ impl Text3d { ) { let mut to_try = to_retry.drain().chain(&changed_text).collect::>(); for id in to_try { + debug!(?id); let Ok((this, font, mut ak_node)) = q.get_mut(id) else { to_retry.insert(id); continue; @@ -446,7 +446,7 @@ impl Text3d { if !fonts.contains(font) { warn!("{font:?} does not (yet) exist. Retrying next frame..."); to_retry.insert(id); - return; + continue; } let Some((mesh, shape)) = cache .entry((text.clone(), xform_key, font.clone())) @@ -496,9 +496,10 @@ impl Text3d { }) .clone() else { - error!("Failed to generate text mesh"); + error!(?text, "Failed to generate text mesh"); continue; }; + debug!(?id, ?mesh, ?shape); cmds.insert((mesh, shape)); } } diff --git a/rs/engine/src/util.rs b/rs/engine/src/util.rs index aa65798..acd4a8b 100644 --- a/rs/engine/src/util.rs +++ b/rs/engine/src/util.rs @@ -954,12 +954,20 @@ macro_rules! state_matches { /// ``` #[macro_export] macro_rules! entity_tree { - ($cmds:ident; ( $($bundles:expr),* $(,)? $(; #children: $($children:tt),* $(,)?)? )) => { - $cmds.spawn(( - $($bundles),* - ))$(.with_children(|cmds| { - $(entity_tree!(cmds; $children);)* - }))? + ($cmds:ident; ( $($bundles:expr),* $(,)? $(=> |$then_cmds:ident| $then:block)? $(; #children: $($children:tt),* $(,)?)? )) => { + { + let mut cmds = $cmds.spawn(( + $($bundles),* + )); + $({ + let mut $then_cmds = &mut cmds; + $then; + };)? + $(cmds.with_children(|cmds| { + $(entity_tree!(cmds; $children);)* + });)? + cmds + } } } @@ -972,7 +980,7 @@ pub fn debug_component_names(In((id, msg)): In<(Entity, String)>, world: &mut Wo .into_iter() .map(ComponentInfo::name) .collect::>(); - debug!(?components, "{msg}"); + debug!(?id, ?components, "{msg}"); } /// [error_component_names] @@ -988,6 +996,9 @@ pub fn error_component_names(In((id, msg)): In<(Entity, String)>, world: &mut Wo error!(?components, "{msg}"); } +// TODO: These could be macros for more flexibility and so the target +// isn't always `sond_has_engine::util`, but it seems like the arguments +// would be moderately complicated. pub trait LogComponentNames { /// Run [debug_component_names] fn debug_components(&mut self, id: Entity, msg: impl std::fmt::Display); diff --git a/rs/src/ui/pause_menu.rs b/rs/src/ui/pause_menu.rs index 487fbb0..d38deff 100644 --- a/rs/src/ui/pause_menu.rs +++ b/rs/src/ui/pause_menu.rs @@ -1,5 +1,13 @@ use crate::player::input::PlayerAction; -use bevy::{ecs::query::QuerySingleError, prelude::*}; +use bevy::{ + app::AppExit, + ecs::{ + query::QuerySingleError, + system::{RunSystemOnce, SystemId}, + }, + pbr::ExtendedMaterial, + prelude::*, +}; use engine::{ entity_tree, input::InputState, @@ -9,30 +17,54 @@ use engine::{ ExtMat, }, ui::{ + focus::{AdjacentWidgets, FocusTarget, Wedge2d}, layout::LineUpChildren, widgets::{ - CuboidPanel, CuboidPanelBundle, Text3d, Text3dBundle, WidgetBundle, WidgetShape, + dbg_event, new_unlit_material, on_ok, Button3dBundle, CuboidFaces, CuboidPanel, + CuboidPanelBundle, InteractHandlers, RectBorderDesc, RectCorners, Text3d, Text3dBundle, + WidgetBundle, WidgetShape, }, - Fade, GlobalUi, MenuStack, UiCam, UiFonts, UiMat, + Fade, FadeCommands, GlobalUi, MenuStack, UiCam, UiFonts, UiMat, }, util::StateStack, }; +use enum_components::{EntityEnumCommands, EnumComponent, WithVariant}; use leafwing_input_manager::action_state::ActionState; use rapier3d::geometry::SharedShape; +use std::ops::ControlFlow; use web_time::Duration; -use engine::ui::FadeCommands; -use engine::ui::widgets::new_unlit_material; pub struct PauseMenuPlugin; impl Plugin for PauseMenuPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, setup) + app.init_resource::() + .add_systems(Startup, setup) .add_systems(Update, show_pause_menu); } } -pub fn setup(mut cmds: Commands, mut mats: ResMut>, ui_fonts: Res) { +#[derive(Resource, Debug)] +pub struct PauseSystems { + pub pause: SystemId, + pub unpause: SystemId, +} + +impl FromWorld for PauseSystems { + fn from_world(world: &mut World) -> Self { + PauseSystems { + pause: world.register_system(pause), + unpause: world.register_system(unpause), + } + } +} + +pub fn setup( + mut cmds: Commands, + mut mats: ResMut>, + mut meshes: ResMut>, + ui_fonts: Res, +) { let material = mats.add(UiMat { extension: default(), base: Matter { @@ -41,18 +73,46 @@ pub fn setup(mut cmds: Commands, mut mats: ResMut>, ui_fonts: Res< }, }); let pause_menu = entity_tree!(cmds; ( + Name::new("PauseMenu"), PauseMenu, CuboidPanelBundle { panel: CuboidPanel:: { size: Vec3::new(12.0, 12.0, 12.0), + borders: CuboidFaces { + front: vec![RectBorderDesc { + colors: Some(RectCorners { + top_right: Color::WHITE, + top_left: Color::GREEN, + bottom_left: Color::BLUE, + bottom_right: Color::RED, + }), + material: mats.add(ExtMat { + base: Matter { + base: default(), + extension: DistanceDither::ui(), + } , + extension: DitherFade::default(), + }), + ..default() + }], + ..default() + }, ..default() }, material, ..default() }, - Fade::ZERO; + Fade::ZERO, + AdjacentWidgets { + prev: Some(FocusTarget::Child(0)), + next: Some(FocusTarget::Child(0)), + directions: vec![ + (Wedge2d::circle(), FocusTarget::Child(0)), + ], + }; #children: ( + Name::new("container"), WidgetBundle { shape: WidgetShape(SharedShape::cuboid(5.5, 1.0, 5.5)), transform: Transform { @@ -61,24 +121,138 @@ pub fn setup(mut cmds: Commands, mut mats: ResMut>, ui_fonts: Res< }, ..default() }, - LineUpChildren::vertical(); - #children: ( - Text3dBundle::> { - text_3d: Text3d { - text: "Game Paused".into(), + LineUpChildren::vertical().with_spacing(0.1), + AdjacentWidgets { + prev: Some(FocusTarget::Child(0)), + next: Some(FocusTarget::Child(0)), + directions: vec![ + (Wedge2d::circle(), FocusTarget::Child(0)), + ], + }; + #children: + ( + Name::new("game_paused_text"), + Text3dBundle::> { + text_3d: Text3d { + text: "Game Paused".into(), + ..default() + }, + material: mats.add(new_unlit_material()), + font: ui_fonts.mono_3d.clone(), ..default() }, - material: mats.add(new_unlit_material()), - font: ui_fonts.mono_3d.clone(), - ..default() - }, - ), + AdjacentWidgets::vertical_siblings(), + ), + ( + Name::new("resume_button"), + Button3dBundle { + shape: WidgetShape(SharedShape::cuboid(3.0, 0.5, 0.5)), + mesh: meshes.add(Cuboid::new(6.0, 1.0, 1.0)), + material: mats.add(UiMat { + extension: DitherFade::default(), + base: Matter { + extension: DistanceDither::ui(), + base: Color::LIME_GREEN.into(), + }, + }), + handlers: InteractHandlers(vec![ + dbg_event(), + on_ok(|cmds| { + cmds.commands().add(|world: &mut World| { + world.run_system_once(unpause); + }); + ControlFlow::Break(()) + }), + ]), + ..default() + }, + AdjacentWidgets::vertical_siblings() + => |cmds| { + cmds.set_enum(pause_menu_widget::ResumeButton); + }; + #children: ( + Name::new("resume_button_text"), + Text3dBundle { + text_3d: Text3d { text: "Resume Game".into(), flat: false, ..default() }, + font: ui_fonts.mono_3d.clone(), + material: mats.add(UiMat { + extension: DitherFade::default(), + base: Matter { + extension: DistanceDither::ui(), + base: Color::WHITE.into(), + }, + }), + transform: Transform { + translation: Vec3::NEG_Y, + ..default() + }, + ..default() + } + ), + ), + ( + Name::new("quit_button"), + Button3dBundle { + shape: WidgetShape(SharedShape::cuboid(2.0, 0.5, 0.5)), + mesh: meshes.add(Cuboid::new(4.0, 1.0, 1.0)), + material: mats.add(UiMat { + extension: DitherFade::default(), + base: Matter { + extension: DistanceDither::ui(), + base: Color::ORANGE_RED.into(), + }, + }), + handlers: InteractHandlers(vec![ + dbg_event(), + on_ok(|cmds| { + cmds.commands().add(|world: &mut World| { + world.resource_mut::>() + .send(AppExit); + }); + ControlFlow::Break(()) + }), + ]), + ..default() + }, + AdjacentWidgets::vertical_siblings() + => |cmds| { + cmds.set_enum(pause_menu_widget::QuitButton); + }; + #children: ( + Name::new("exit_button_text"), + Text3dBundle { + text_3d: Text3d { text: "Exit Game".into(), flat: false, ..default() }, + font: ui_fonts.mono_3d.clone(), + material: mats.add(UiMat { + extension: DitherFade::default(), + base: Matter { + extension: DistanceDither::ui(), + base: Color::WHITE.into(), + }, + }), + transform: Transform { + translation: Vec3::NEG_Y, + ..default() + }, + ..default() + } + ), + ), ), )) + .with_enum(pause_menu_widget::Panel) .id(); cmds.insert_resource(PauseMenuId(pause_menu)); } +#[derive(EnumComponent)] +#[component(mutable, derive(Debug, PartialEq, Eq))] +pub enum PauseMenuWidget { + Panel, + ResumeButton, + QuitButton, +} + #[derive(Resource, Deref, DerefMut, Debug, Copy, Clone, PartialEq, Eq)] pub struct PauseMenuId(pub Entity); @@ -88,29 +262,63 @@ pub struct PauseMenu; pub fn show_pause_menu( mut cmds: Commands, - mut stack: Query<&mut MenuStack, With>, - mut cam: Query<&mut UiCam, With>, - id: Res, + stack: Query<&MenuStack, With>, + panel: Query>, actions_q: Query<&ActionState>, - mut states: ResMut>, + states: Res>, + systems: Res, ) { - let id = **id; + let id = panel.single(); for actions in &actions_q { if actions.just_pressed(&PlayerAction::PauseGame) { - let mut stack = stack.single_mut(); - let mut cam = cam.single_mut(); + let mut stack = stack.single(); if stack.last() == Some(&id) { - debug_assert_eq!(states.last(), Some(&InputState::InMenu)); - cmds.entity(id).fade_out_secs(0.5); - states.pop(); - stack.pop(); - cam.focus = stack.last().copied(); + cmds.run_system(systems.unpause); } else if states.last() == Some(&InputState::InGame) { - cmds.entity(id).fade_in_secs(0.5); - states.push(InputState::InMenu); - stack.push(id); - cam.focus = Some(id); + cmds.run_system(systems.pause); } } } } + +pub fn pause( + mut cmds: Commands, + mut stack: Query<&mut MenuStack, With>, + mut cam: Query<&mut UiCam, With>, + panel: Query>, + resume_btn: Query>, + mut states: ResMut>, +) { + let id = panel.single(); + let mut stack = stack.single_mut(); + let mut cam = cam.single_mut(); + if states.last() == Some(&InputState::InGame) { + cmds.entity(id).fade_in_secs(0.5); + states.push(InputState::InMenu); + stack.push(id); + cam.focus = Some(resume_btn.single()); + } else { + error!("Can't pause game while it's not running"); + } +} + +pub fn unpause( + mut cmds: Commands, + mut stack: Query<&mut MenuStack, With>, + mut cam: Query<&mut UiCam, With>, + panel: Query>, + mut states: ResMut>, +) { + let id = panel.single(); + let mut stack = stack.single_mut(); + let mut cam = cam.single_mut(); + if stack.last() == Some(&id) { + debug_assert_eq!(states.last(), Some(&InputState::InMenu)); + cmds.entity(id).fade_out_secs(0.5); + states.pop(); + stack.pop(); + cam.focus = stack.last().copied(); + } else { + error!("Pause menu isn't the top of the MenuStack"); + } +}