From 1dc9554d98d65ed2fee6cf722c1958db89cb9d13 Mon Sep 17 00:00:00 2001 From: Codaishin Date: Fri, 17 Jan 2025 20:55:44 +0100 Subject: [PATCH 1/5] feat: improve control over triggering clickable --- src/components/clickable.rs | 289 +++++++++++++++++++++++++++--------- src/components/tile.rs | 7 +- src/main.rs | 7 +- src/traits.rs | 1 + src/traits/get_key.rs | 5 + 5 files changed, 234 insertions(+), 75 deletions(-) create mode 100644 src/traits/get_key.rs diff --git a/src/components/clickable.rs b/src/components/clickable.rs index 3f5ee0d..8ee1d1d 100644 --- a/src/components/clickable.rs +++ b/src/components/clickable.rs @@ -1,28 +1,39 @@ +use std::{hash::Hash, marker::PhantomData}; + use crate::{ resources::mouse_world_position::MouseWorldPosition, traits::{ asset_handle::AssetHandle, + get_key::GetKey, is_point_hit::{IsPointHit, Relative}, }, }; use bevy::prelude::*; #[derive(Component, Debug, PartialEq, Default)] -pub struct Clickable { +pub struct Clickable +where + TKeyDefinition: GetKey, +{ clicked: bool, + _p: PhantomData, } -impl Clickable { +impl Clickable +where + TKeyDefinition: GetKey + Sync + Send + 'static, + TKeyDefinition::TKey: Copy + Eq + Hash + Send + Sync + 'static, +{ pub fn update_using( mut entities: Query<(&mut Self, &TCollider, &Transform)>, colliders: Res>, mouse_world_position: Res, - mouse_input: Res>, + input: Res>, ) where TCollider: Component + AssetHandle, TCollider::TAsset: IsPointHit, { - if !mouse_input.pressed(MouseButton::Right) { + if !input.pressed(TKeyDefinition::get_key()) { return; } @@ -46,11 +57,11 @@ impl Clickable { pub fn toggle( mut commands: Commands, - entities: Query<(Entity, &Clickable, Option<&TComponent>), Changed>, + entities: Query<(Entity, &Self, Option<&TComponent>), Changed>, ) where TComponent: Component + Default, { - for (entity, Clickable { clicked }, component) in &entities { + for (entity, Self { clicked, .. }, component) in &entities { if !clicked { continue; } @@ -67,6 +78,28 @@ impl Clickable { } } +#[derive(Debug, PartialEq, Default)] +pub struct MouseLeft; + +impl GetKey for MouseLeft { + type TKey = MouseButton; + + fn get_key() -> Self::TKey { + const { MouseButton::Left } + } +} + +#[derive(Debug, PartialEq, Default)] +pub struct MouseRight; + +impl GetKey for MouseRight { + type TKey = MouseButton; + + fn get_key() -> Self::TKey { + const { MouseButton::Right } + } +} + #[cfg(test)] mod test_update { use super::*; @@ -74,6 +107,20 @@ mod test_update { use bevy::ecs::system::{RunSystemError, RunSystemOnce}; use mockall::{automock, predicate::eq}; + #[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy)] + struct _DeviceKey; + + #[derive(Debug, PartialEq, Eq, Default)] + struct _Button; + + impl GetKey for _Button { + type TKey = _DeviceKey; + + fn get_key() -> Self::TKey { + _DeviceKey + } + } + #[derive(Asset, TypePath)] struct _ColliderAsset { mock: Mock_ColliderAsset, @@ -108,31 +155,31 @@ mod test_update { } } - enum _MouseClick { - RightJustNot(Option), - RightHold(Option), - Nothing, + enum _Device { + Pressed(Option), + Held(Option), + Rest, } fn setup( handle: &Handle<_ColliderAsset>, collider_asset: _ColliderAsset, - mouse_click: _MouseClick, + mouse_click: _Device, ) -> App { let mut app = App::new().single_threaded(Update); let mut assets = Assets::<_ColliderAsset>::default(); - let mut mouse_input = ButtonInput::::default(); + let mut mouse_input = ButtonInput::<_DeviceKey>::default(); let mouse_position = MouseWorldPosition(match mouse_click { - _MouseClick::RightJustNot(mouse_position) => { - mouse_input.press(MouseButton::Right); + _Device::Pressed(mouse_position) => { + mouse_input.press(_DeviceKey); mouse_position } - _MouseClick::RightHold(mouse_position) => { - mouse_input.press(MouseButton::Right); - mouse_input.clear_just_pressed(MouseButton::Right); + _Device::Held(mouse_position) => { + mouse_input.press(_DeviceKey); + mouse_input.clear_just_pressed(_DeviceKey); mouse_position } - _MouseClick::Nothing => Some(Vec2::default()), + _Device::Rest => Some(Vec2::default()), }); assets.insert(handle, collider_asset); @@ -147,18 +194,27 @@ mod test_update { fn set_to_not_clicked_when_not_hit() -> Result<(), RunSystemError> { let asset = _ColliderAsset::default(); let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::RightJustNot(Some(Vec2::ZERO))); + let mut app = setup(&handle, asset, _Device::Pressed(Some(Vec2::ZERO))); let entity = app .world_mut() - .spawn((Clickable { clicked: true }, _Collider(handle))) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Collider(handle), + )) .id(); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; assert_eq!( - Some(&Clickable { clicked: false }), - app.world().entity(entity).get::(), + Some(&Clickable::<_Button> { + clicked: false, + ..default() + }), + app.world().entity(entity).get::>(), ); Ok(()) } @@ -167,13 +223,13 @@ mod test_update { fn do_not_insert_clicked_when_clicked_not_already_present() -> Result<(), RunSystemError> { let asset = _ColliderAsset::default(); let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::RightJustNot(Some(Vec2::ZERO))); + let mut app = setup(&handle, asset, _Device::Pressed(Some(Vec2::ZERO))); let entity = app.world_mut().spawn(_Collider(handle)).id(); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; - assert_eq!(None, app.world().entity(entity).get::()); + assert_eq!(None, app.world().entity(entity).get::>()); Ok(()) } @@ -185,18 +241,27 @@ mod test_update { }), }; let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::RightJustNot(Some(Vec2::ZERO))); + let mut app = setup(&handle, asset, _Device::Pressed(Some(Vec2::ZERO))); let entity = app .world_mut() - .spawn((Clickable { clicked: false }, _Collider(handle))) + .spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Collider(handle), + )) .id(); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; assert_eq!( - Some(&Clickable { clicked: true }), - app.world().entity(entity).get::(), + Some(&Clickable::<_Button> { + clicked: true, + ..default() + }), + app.world().entity(entity).get::>(), ); Ok(()) } @@ -213,16 +278,17 @@ mod test_update { }; let handle = new_handle!(_ColliderAsset); - let mut app = setup( - &handle, - asset, - _MouseClick::RightJustNot(Some(Vec2::new(1., 2.))), - ); - app.world_mut() - .spawn((Clickable { clicked: false }, _Collider(handle))); + let mut app = setup(&handle, asset, _Device::Pressed(Some(Vec2::new(1., 2.)))); + app.world_mut().spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Collider(handle), + )); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>) + .run_system_once(Clickable::<_Button>::update_using::<_Collider>) } #[test] @@ -237,19 +303,18 @@ mod test_update { }; let handle = new_handle!(_ColliderAsset); - let mut app = setup( - &handle, - asset, - _MouseClick::RightJustNot(Some(Vec2::new(1., 2.))), - ); + let mut app = setup(&handle, asset, _Device::Pressed(Some(Vec2::new(1., 2.)))); app.world_mut().spawn(( - Clickable { clicked: false }, + Clickable::<_Button> { + clicked: false, + ..default() + }, _Collider(handle), Transform::from_xyz(3., 3., 0.), )); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>) + .run_system_once(Clickable::<_Button>::update_using::<_Collider>) } #[test] @@ -260,18 +325,27 @@ mod test_update { }), }; let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::Nothing); + let mut app = setup(&handle, asset, _Device::Rest); let entity = app .world_mut() - .spawn((Clickable { clicked: false }, _Collider(handle))) + .spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Collider(handle), + )) .id(); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; assert_eq!( - Some(&Clickable { clicked: false }), - app.world().entity(entity).get::(), + Some(&Clickable::<_Button> { + clicked: false, + ..default() + }), + app.world().entity(entity).get::>(), ); Ok(()) } @@ -284,18 +358,27 @@ mod test_update { }), }; let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::RightHold(Some(Vec2::ZERO))); + let mut app = setup(&handle, asset, _Device::Held(Some(Vec2::ZERO))); let entity = app .world_mut() - .spawn((Clickable { clicked: false }, _Collider(handle))) + .spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Collider(handle), + )) .id(); app.world_mut() - .run_system_once(Clickable::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; assert_eq!( - Some(&Clickable { clicked: true }), - app.world().entity(entity).get::(), + Some(&Clickable::<_Button> { + clicked: true, + ..default() + }), + app.world().entity(entity).get::>(), ); Ok(()) } @@ -304,7 +387,7 @@ mod test_update { struct _Changed(bool); impl _Changed { - fn detect(mut commands: Commands, entities: Query<(Entity, Ref)>) { + fn detect(mut commands: Commands, entities: Query<(Entity, Ref>)>) { for (entity, clickable) in &entities { let mut entity = commands.entity(entity); entity.insert(_Changed(clickable.is_changed())); @@ -320,15 +403,25 @@ mod test_update { }), }; let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _MouseClick::RightHold(Some(Vec2::ZERO))); + let mut app = setup(&handle, asset, _Device::Held(Some(Vec2::ZERO))); let entity = app .world_mut() - .spawn((Clickable { clicked: true }, _Collider(handle))) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Collider(handle), + )) .id(); app.add_systems( Update, - (Clickable::update_using::<_Collider>, _Changed::detect).chain(), + ( + Clickable::<_Button>::update_using::<_Collider>, + _Changed::detect, + ) + .chain(), ); app.update(); app.update(); @@ -346,12 +439,26 @@ mod test_toggle { use crate::test_tools::SingleThreaded; use std::ops::DerefMut; + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] + struct _DeviceKey; + + #[derive(Debug, PartialEq, Default)] + struct _Button; + + impl GetKey for _Button { + type TKey = _DeviceKey; + + fn get_key() -> Self::TKey { + _DeviceKey + } + } + #[derive(Component, Debug, PartialEq, Default)] struct _Component; fn setup() -> App { let mut app = App::new().single_threaded(Update); - app.add_systems(Update, Clickable::toggle::<_Component>); + app.add_systems(Update, Clickable::<_Button>::toggle::<_Component>); app } @@ -359,7 +466,13 @@ mod test_toggle { #[test] fn insert_component_when_clicked() { let mut app = setup(); - let entity = app.world_mut().spawn(Clickable { clicked: true }).id(); + let entity = app + .world_mut() + .spawn(Clickable::<_Button> { + clicked: true, + ..default() + }) + .id(); app.update(); @@ -372,7 +485,13 @@ mod test_toggle { #[test] fn insert_component_when_clicked_only_once() { let mut app = setup(); - let entity = app.world_mut().spawn(Clickable { clicked: true }).id(); + let entity = app + .world_mut() + .spawn(Clickable::<_Button> { + clicked: true, + ..default() + }) + .id(); app.update(); app.world_mut().entity_mut(entity).remove::<_Component>(); @@ -384,12 +503,18 @@ mod test_toggle { #[test] fn insert_component_when_clicked_again_after_mut_deref() { let mut app = setup(); - let entity = app.world_mut().spawn(Clickable { clicked: true }).id(); + let entity = app + .world_mut() + .spawn(Clickable::<_Button> { + clicked: true, + ..default() + }) + .id(); app.update(); app.world_mut().entity_mut(entity).remove::<_Component>(); let mut clickable = app.world_mut().entity_mut(entity); - let mut clickable = clickable.get_mut::().unwrap(); + let mut clickable = clickable.get_mut::>().unwrap(); clickable.deref_mut(); app.update(); @@ -404,7 +529,13 @@ mod test_toggle { let mut app = setup(); let entity = app .world_mut() - .spawn((Clickable { clicked: false }, _Component)) + .spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Component, + )) .id(); app.update(); @@ -420,7 +551,13 @@ mod test_toggle { let mut app = setup(); let entity = app .world_mut() - .spawn((Clickable { clicked: true }, _Component)) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Component, + )) .id(); app.update(); @@ -433,7 +570,13 @@ mod test_toggle { let mut app = setup(); let entity = app .world_mut() - .spawn((Clickable { clicked: true }, _Component)) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Component, + )) .id(); app.update(); @@ -449,12 +592,18 @@ mod test_toggle { #[test] fn remove_component_when_clicked_again_after_mut_deref() { let mut app = setup(); - let entity = app.world_mut().spawn(Clickable { clicked: true }).id(); + let entity = app + .world_mut() + .spawn(Clickable::<_Button> { + clicked: true, + ..default() + }) + .id(); app.update(); app.world_mut().entity_mut(entity).insert(_Component); let mut clickable = app.world_mut().entity_mut(entity); - let mut clickable = clickable.get_mut::().unwrap(); + let mut clickable = clickable.get_mut::>().unwrap(); clickable.deref_mut(); app.update(); diff --git a/src/components/tile.rs b/src/components/tile.rs index eef9db1..2146e76 100644 --- a/src/components/tile.rs +++ b/src/components/tile.rs @@ -1,4 +1,7 @@ -use super::{clickable::Clickable, use_asset::UseAsset}; +use super::{ + clickable::{Clickable, MouseRight}, + use_asset::UseAsset, +}; use crate::assets::tile_collider_definition::TileColliderDefinition; use bevy::prelude::*; use std::path::Path; @@ -10,7 +13,7 @@ use std::path::Path; UseAsset(Tile::asset), UseAsset(Tile::asset), UseAsset(Tile::asset), - Clickable, + Clickable, )] pub struct Tile; diff --git a/src/main.rs b/src/main.rs index 29092fd..ecd2ffa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use project_zyheeda_pathfinding::{ asset_loader::CustomAssetLoader, assets::{grid::Grid, tile_collider_definition::TileColliderDefinition}, components::{ - clickable::Clickable, + clickable::{Clickable, MouseLeft, MouseRight}, obstacle::Obstacle, player_camera::PlayerCamera, tile_builder::TileBuilder, @@ -33,8 +33,9 @@ fn main() -> AppExit { .add_systems( Update, ( - Clickable::update_using::, - Clickable::toggle::, + Clickable::::update_using::, + Clickable::::update_using::, + Clickable::::toggle::, Obstacle::update_color, UseAsset::::insert_system, UseAsset::::insert_system, diff --git a/src/traits.rs b/src/traits.rs index 73c8e4a..74f9e5a 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,6 @@ pub mod asset_handle; pub mod concat; +pub mod get_key; pub mod get_mouse_ray; pub mod into_component; pub mod is_point_hit; diff --git a/src/traits/get_key.rs b/src/traits/get_key.rs new file mode 100644 index 0000000..d0b1ed2 --- /dev/null +++ b/src/traits/get_key.rs @@ -0,0 +1,5 @@ +pub trait GetKey { + type TKey; + + fn get_key() -> Self::TKey; +} From 6ea753880c5f35cbb42c3d208ec8e8e7c6cb9ead Mon Sep 17 00:00:00 2001 From: Codaishin Date: Fri, 17 Jan 2025 21:08:56 +0100 Subject: [PATCH 2/5] refactor: improve systems layout and readability --- src/components/clickable.rs | 18 +++++++++--------- src/components/use_asset.rs | 4 ++-- src/main.rs | 22 ++++++++++++++++------ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/components/clickable.rs b/src/components/clickable.rs index 8ee1d1d..fdc9c9d 100644 --- a/src/components/clickable.rs +++ b/src/components/clickable.rs @@ -24,7 +24,7 @@ where TKeyDefinition: GetKey + Sync + Send + 'static, TKeyDefinition::TKey: Copy + Eq + Hash + Send + Sync + 'static, { - pub fn update_using( + pub fn detect_click_on( mut entities: Query<(&mut Self, &TCollider, &Transform)>, colliders: Res>, mouse_world_position: Res, @@ -207,7 +207,7 @@ mod test_update { .id(); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>)?; assert_eq!( Some(&Clickable::<_Button> { @@ -227,7 +227,7 @@ mod test_update { let entity = app.world_mut().spawn(_Collider(handle)).id(); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>)?; assert_eq!(None, app.world().entity(entity).get::>()); Ok(()) @@ -254,7 +254,7 @@ mod test_update { .id(); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>)?; assert_eq!( Some(&Clickable::<_Button> { @@ -288,7 +288,7 @@ mod test_update { )); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>) + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>) } #[test] @@ -314,7 +314,7 @@ mod test_update { )); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>) + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>) } #[test] @@ -338,7 +338,7 @@ mod test_update { .id(); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>)?; assert_eq!( Some(&Clickable::<_Button> { @@ -371,7 +371,7 @@ mod test_update { .id(); app.world_mut() - .run_system_once(Clickable::<_Button>::update_using::<_Collider>)?; + .run_system_once(Clickable::<_Button>::detect_click_on::<_Collider>)?; assert_eq!( Some(&Clickable::<_Button> { @@ -418,7 +418,7 @@ mod test_update { app.add_systems( Update, ( - Clickable::<_Button>::update_using::<_Collider>, + Clickable::<_Button>::detect_click_on::<_Collider>, _Changed::detect, ) .chain(), diff --git a/src/components/use_asset.rs b/src/components/use_asset.rs index 57eae61..6df141d 100644 --- a/src/components/use_asset.rs +++ b/src/components/use_asset.rs @@ -24,7 +24,7 @@ where } } - pub fn insert_system( + pub fn insert( mut commands: Commands, asset_server: Res, entities: Query<(Entity, &Self), Changed>, @@ -102,7 +102,7 @@ mod tests { fn setup() -> App { let mut app = App::new().single_threaded(Update); app.init_resource::<_AssetServer>(); - app.add_systems(Update, _UseAsset::insert_system); + app.add_systems(Update, _UseAsset::insert); app } diff --git a/src/main.rs b/src/main.rs index ecd2ffa..021e985 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,14 +33,24 @@ fn main() -> AppExit { .add_systems( Update, ( - Clickable::::update_using::, - Clickable::::update_using::, + UseAsset::::insert, + UseAsset::::insert, + UseAsset::::insert, + UseAsset::::insert.after(Obstacle::update_color), + ), + ) + .add_systems( + Update, + ( + Clickable::::detect_click_on::, + Clickable::::detect_click_on::, + ), + ) + .add_systems( + Update, + ( Clickable::::toggle::, Obstacle::update_color, - UseAsset::::insert_system, - UseAsset::::insert_system, - UseAsset::::insert_system, - UseAsset::::insert_system, ) .chain(), ); From f83af0cf130e5d6acc2e6f8803417924e63b0d69 Mon Sep 17 00:00:00 2001 From: Codaishin Date: Fri, 17 Jan 2025 22:42:13 +0100 Subject: [PATCH 3/5] feat: toggle start --- assets/start.json | 3 + src/components.rs | 2 +- src/components/clickable.rs | 154 +++++++++++++++------------------- src/components/obstacle.rs | 160 ------------------------------------ src/components/tile.rs | 6 +- src/components/tile_type.rs | 125 ++++++++++++++++++++++++++++ src/main.rs | 9 +- 7 files changed, 204 insertions(+), 255 deletions(-) create mode 100644 assets/start.json delete mode 100644 src/components/obstacle.rs create mode 100644 src/components/tile_type.rs diff --git a/assets/start.json b/assets/start.json new file mode 100644 index 0000000..a86e037 --- /dev/null +++ b/assets/start.json @@ -0,0 +1,3 @@ +{ + "color": "#0E9F6C" +} diff --git a/src/components.rs b/src/components.rs index 318b640..aeddfa1 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,8 +1,8 @@ pub mod clickable; -pub mod obstacle; pub mod player_camera; pub mod tile; pub mod tile_builder; pub mod tile_collider; pub mod tile_grid; +pub mod tile_type; pub mod use_asset; diff --git a/src/components/clickable.rs b/src/components/clickable.rs index fdc9c9d..d66ec3c 100644 --- a/src/components/clickable.rs +++ b/src/components/clickable.rs @@ -56,24 +56,22 @@ where } pub fn toggle( - mut commands: Commands, - entities: Query<(Entity, &Self, Option<&TComponent>), Changed>, - ) where - TComponent: Component + Default, + toggle_state: TComponent, + ) -> impl Fn(Query<(&Self, &mut TComponent), Changed>) + where + TComponent: Component + Default + PartialEq + Copy, { - for (entity, Self { clicked, .. }, component) in &entities { - if !clicked { - continue; + move |mut entities| { + for (Self { clicked, .. }, mut component) in &mut entities { + if !clicked { + continue; + } + + *component = match *component == toggle_state { + true => TComponent::default(), + false => toggle_state, + } } - - let Some(mut entity) = commands.get_entity(entity) else { - continue; - }; - - match component { - Some(_) => entity.remove::(), - None => entity.try_insert(TComponent::default()), - }; } } } @@ -453,120 +451,123 @@ mod test_toggle { } } - #[derive(Component, Debug, PartialEq, Default)] - struct _Component; + #[derive(Component, Debug, PartialEq, Default, Clone, Copy)] + enum _Component { + #[default] + ToggleOff, + ToggleOn, + } fn setup() -> App { let mut app = App::new().single_threaded(Update); - app.add_systems(Update, Clickable::<_Button>::toggle::<_Component>); + app.add_systems(Update, Clickable::<_Button>::toggle(_Component::ToggleOn)); app } #[test] - fn insert_component_when_clicked() { + fn toggle_on_when_clicked() { let mut app = setup(); let entity = app .world_mut() - .spawn(Clickable::<_Button> { - clicked: true, - ..default() - }) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Component::ToggleOff, + )) .id(); app.update(); assert_eq!( - Some(&_Component), + Some(&_Component::ToggleOn), app.world().entity(entity).get::<_Component>(), ); } #[test] - fn insert_component_when_clicked_only_once() { + fn toggle_on_when_clicked_only_once() { let mut app = setup(); let entity = app .world_mut() - .spawn(Clickable::<_Button> { - clicked: true, - ..default() - }) - .id(); - - app.update(); - app.world_mut().entity_mut(entity).remove::<_Component>(); - app.update(); - - assert_eq!(None, app.world().entity(entity).get::<_Component>()); - } - - #[test] - fn insert_component_when_clicked_again_after_mut_deref() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(Clickable::<_Button> { - clicked: true, - ..default() - }) + .spawn(( + Clickable::<_Button> { + clicked: true, + ..default() + }, + _Component::ToggleOff, + )) .id(); app.update(); - app.world_mut().entity_mut(entity).remove::<_Component>(); - let mut clickable = app.world_mut().entity_mut(entity); - let mut clickable = clickable.get_mut::>().unwrap(); - clickable.deref_mut(); + app.world_mut() + .entity_mut(entity) + .insert(_Component::ToggleOff); app.update(); assert_eq!( - Some(&_Component), + Some(&_Component::ToggleOff), app.world().entity(entity).get::<_Component>() ); } #[test] - fn do_nothing_when_not_clicked() { + fn toggle_on_when_clicked_again_after_mut_deref() { let mut app = setup(); let entity = app .world_mut() .spawn(( Clickable::<_Button> { - clicked: false, + clicked: true, ..default() }, - _Component, + _Component::ToggleOff, )) .id(); app.update(); + app.world_mut() + .entity_mut(entity) + .insert(_Component::ToggleOff); + app.world_mut() + .entity_mut(entity) + .get_mut::>() + .unwrap() + .deref_mut(); + app.update(); assert_eq!( - Some(&_Component), + Some(&_Component::ToggleOn), app.world().entity(entity).get::<_Component>() ); } #[test] - fn remove_component_when_clicked() { + fn do_nothing_when_not_clicked() { let mut app = setup(); let entity = app .world_mut() .spawn(( Clickable::<_Button> { - clicked: true, + clicked: false, ..default() }, - _Component, + _Component::ToggleOff, )) .id(); app.update(); - assert_eq!(None, app.world().entity(entity).get::<_Component>()); + assert_eq!( + Some(&_Component::ToggleOff), + app.world().entity(entity).get::<_Component>() + ); } #[test] - fn remove_component_when_clicked_only_once() { + fn toggle_off_when_clicked() { let mut app = setup(); let entity = app .world_mut() @@ -575,38 +576,15 @@ mod test_toggle { clicked: true, ..default() }, - _Component, + _Component::ToggleOn, )) .id(); app.update(); - app.world_mut().entity_mut(entity).insert(_Component); - app.update(); assert_eq!( - Some(&_Component), - app.world().entity(entity).get::<_Component>(), + Some(&_Component::ToggleOff), + app.world().entity(entity).get::<_Component>() ); } - - #[test] - fn remove_component_when_clicked_again_after_mut_deref() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(Clickable::<_Button> { - clicked: true, - ..default() - }) - .id(); - - app.update(); - app.world_mut().entity_mut(entity).insert(_Component); - let mut clickable = app.world_mut().entity_mut(entity); - let mut clickable = clickable.get_mut::>().unwrap(); - clickable.deref_mut(); - app.update(); - - assert_eq!(None, app.world().entity(entity).get::<_Component>()); - } } diff --git a/src/components/obstacle.rs b/src/components/obstacle.rs deleted file mode 100644 index 9695267..0000000 --- a/src/components/obstacle.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::components::use_asset::UseAsset; -use bevy::prelude::*; -use std::path::Path; - -#[derive(Component, Debug, PartialEq, Default)] -pub struct Obstacle; - -impl Obstacle { - const ASSET_PATH: &str = "obstacle.json"; - - fn asset() -> UseAsset { - UseAsset::new(Path::new(Self::ASSET_PATH)) - } - - pub fn update_color( - mut commands: Commands, - obstacles: Query<(Entity, Option<&UseAsset>), Added>, - mut removed_obstacles: RemovedComponents, - original_colors: Query<&OriginalColor>, - ) { - for (entity, color) in &obstacles { - let Some(mut entity) = commands.get_entity(entity) else { - continue; - }; - - entity.try_insert(Obstacle::asset()); - - let Some(color) = color else { - continue; - }; - - entity.try_insert(OriginalColor(color.clone())); - } - - for entity in removed_obstacles.read() { - let Ok(OriginalColor(color)) = original_colors.get(entity) else { - continue; - }; - let Some(mut entity) = commands.get_entity(entity) else { - continue; - }; - - entity.try_insert(color.clone()); - entity.remove::(); - } - } -} - -#[derive(Component, Debug, PartialEq)] -pub struct OriginalColor(UseAsset); - -#[cfg(test)] -mod test { - use super::*; - use crate::test_tools::SingleThreaded; - - fn setup() -> App { - let mut app = App::new().single_threaded(Update); - app.add_systems(Update, Obstacle::update_color); - - app - } - - #[test] - fn insert_color_asset() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(( - Obstacle, - UseAsset::::new(Path::new("some/other")), - )) - .id(); - - app.update(); - - assert_eq!( - Some(&Obstacle::asset()), - app.world().entity(entity).get::>(), - ); - } - - #[test] - fn do_not_insert_obstacle_asset_on_other_entities() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(UseAsset::::new(Path::new("some/other"))) - .id(); - - app.update(); - - assert_eq!( - Some(&UseAsset::::new(Path::new("some/other"))), - app.world().entity(entity).get::>(), - ); - } - - #[test] - fn insert_color_asset_only_once() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(( - Obstacle, - UseAsset::::new(Path::new("some/other")), - )) - .id(); - - app.update(); - app.world_mut() - .entity_mut(entity) - .remove::>(); - app.update(); - - assert_eq!( - None, - app.world().entity(entity).get::>(), - ); - } - - #[test] - fn insert_original_color_asset_when_removed() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(( - Obstacle, - UseAsset::::new(Path::new("some/other")), - )) - .id(); - - app.update(); - app.world_mut().entity_mut(entity).remove::(); - app.update(); - - assert_eq!( - Some(&UseAsset::::new(Path::new("some/other"))), - app.world().entity(entity).get::>(), - ); - } - - #[test] - fn cleanup_original_color_helper_component() { - let mut app = setup(); - let entity = app - .world_mut() - .spawn(( - Obstacle, - UseAsset::::new(Path::new("some/other")), - )) - .id(); - - app.update(); - app.world_mut().entity_mut(entity).remove::(); - app.update(); - - assert_eq!(None, app.world().entity(entity).get::()); - } -} diff --git a/src/components/tile.rs b/src/components/tile.rs index 2146e76..0384e1d 100644 --- a/src/components/tile.rs +++ b/src/components/tile.rs @@ -1,5 +1,6 @@ use super::{ - clickable::{Clickable, MouseRight}, + clickable::{Clickable, MouseLeft, MouseRight}, + tile_type::TileType, use_asset::UseAsset, }; use crate::assets::tile_collider_definition::TileColliderDefinition; @@ -11,8 +12,9 @@ use std::path::Path; Transform, Visibility, UseAsset(Tile::asset), - UseAsset(Tile::asset), UseAsset(Tile::asset), + TileType, + Clickable, Clickable, )] pub struct Tile; diff --git a/src/components/tile_type.rs b/src/components/tile_type.rs new file mode 100644 index 0000000..80eae16 --- /dev/null +++ b/src/components/tile_type.rs @@ -0,0 +1,125 @@ +use super::tile::Tile; +use crate::components::use_asset::UseAsset; +use bevy::prelude::*; +use std::path::Path; + +#[derive(Component, Debug, PartialEq, Default, Clone, Copy)] +pub enum TileType { + #[default] + Walkable, + Obstacle, + Start, +} + +impl From for UseAsset { + fn from(value: TileType) -> Self { + match value { + TileType::Walkable => Tile::asset(), + TileType::Obstacle => UseAsset::new(Path::new("obstacle.json")), + TileType::Start => UseAsset::new(Path::new("start.json")), + } + } +} + +impl TileType { + pub fn update_color(mut commands: Commands, obstacles: Query<(Entity, &Self), Changed>) { + for (entity, tile_type) in &obstacles { + let Some(mut entity) = commands.get_entity(entity) else { + continue; + }; + + entity.try_insert(UseAsset::from(*tile_type)); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_tools::SingleThreaded; + use std::ops::DerefMut; + + fn setup() -> App { + let mut app = App::new().single_threaded(Update); + app.add_systems(Update, TileType::update_color); + + app + } + + #[test] + fn insert_default_color() { + let mut app = setup(); + let entity = app.world_mut().spawn(TileType::Walkable).id(); + + app.update(); + + assert_eq!( + Some(&UseAsset::from(TileType::Walkable)), + app.world().entity(entity).get::>(), + ); + } + + #[test] + fn insert_obstacle_color() { + let mut app = setup(); + let entity = app.world_mut().spawn(TileType::Obstacle).id(); + + app.update(); + + assert_eq!( + Some(&UseAsset::from(TileType::Obstacle)), + app.world().entity(entity).get::>(), + ); + } + + #[test] + fn insert_color_asset_only_once() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + TileType::Obstacle, + UseAsset::::new(Path::new("some/other")), + )) + .id(); + + app.update(); + app.world_mut() + .entity_mut(entity) + .remove::>(); + app.update(); + + assert_eq!( + None, + app.world().entity(entity).get::>(), + ); + } + + #[test] + fn insert_color_asset_again_after_mut_deref() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + TileType::Obstacle, + UseAsset::::new(Path::new("some/other")), + )) + .id(); + + app.update(); + app.world_mut() + .entity_mut(entity) + .get_mut::() + .unwrap() + .deref_mut(); + app.world_mut() + .entity_mut(entity) + .remove::>(); + app.update(); + + assert_eq!( + Some(&UseAsset::from(TileType::Obstacle)), + app.world().entity(entity).get::>(), + ); + } +} diff --git a/src/main.rs b/src/main.rs index 021e985..ef06952 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,11 @@ use project_zyheeda_pathfinding::{ assets::{grid::Grid, tile_collider_definition::TileColliderDefinition}, components::{ clickable::{Clickable, MouseLeft, MouseRight}, - obstacle::Obstacle, player_camera::PlayerCamera, tile_builder::TileBuilder, tile_collider::TileCollider, tile_grid::TileGrid, + tile_type::TileType, use_asset::UseAsset, }, dtos::{grid_layout::GridLayout, tile_color::TileColor, tile_size::TileSize}, @@ -36,7 +36,7 @@ fn main() -> AppExit { UseAsset::::insert, UseAsset::::insert, UseAsset::::insert, - UseAsset::::insert.after(Obstacle::update_color), + UseAsset::::insert.after(TileType::update_color), ), ) .add_systems( @@ -49,8 +49,9 @@ fn main() -> AppExit { .add_systems( Update, ( - Clickable::::toggle::, - Obstacle::update_color, + Clickable::::toggle(TileType::Obstacle), + Clickable::::toggle(TileType::Start), + TileType::update_color, ) .chain(), ); From fcdbc2b7e13abb609a918caee1b7ec1c3c8e5eb5 Mon Sep 17 00:00:00 2001 From: Codaishin Date: Fri, 17 Jan 2025 23:30:07 +0100 Subject: [PATCH 4/5] fix: can repeatedly toggle single tile --- src/components/clickable.rs | 105 ++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/src/components/clickable.rs b/src/components/clickable.rs index d66ec3c..6a789a5 100644 --- a/src/components/clickable.rs +++ b/src/components/clickable.rs @@ -1,5 +1,3 @@ -use std::{hash::Hash, marker::PhantomData}; - use crate::{ resources::mouse_world_position::MouseWorldPosition, traits::{ @@ -9,6 +7,7 @@ use crate::{ }, }; use bevy::prelude::*; +use std::{hash::Hash, marker::PhantomData}; #[derive(Component, Debug, PartialEq, Default)] pub struct Clickable @@ -24,6 +23,37 @@ where TKeyDefinition: GetKey + Sync + Send + 'static, TKeyDefinition::TKey: Copy + Eq + Hash + Send + Sync + 'static, { + fn set_clicked( + mut clickable: Mut>, + collider: &TCollider, + transform: &Transform, + colliders: &Res>, + mouse_position: Vec2, + ) where + TCollider: Component + AssetHandle, + TCollider::TAsset: IsPointHit, + { + let Some(collider) = colliders.get(collider.get_handle()) else { + return; + }; + + let relative_mouse_position = Relative::position(mouse_position).to(transform); + + if clickable.clicked == collider.is_point_hit(relative_mouse_position) { + return; + }; + + clickable.clicked = !clickable.clicked; + } + + fn set_not_clicked(mut clickable: Mut>) { + if !clickable.clicked { + return; + } + + clickable.clicked = false; + } + pub fn detect_click_on( mut entities: Query<(&mut Self, &TCollider, &Transform)>, colliders: Res>, @@ -33,25 +63,17 @@ where TCollider: Component + AssetHandle, TCollider::TAsset: IsPointHit, { - if !input.pressed(TKeyDefinition::get_key()) { - return; - } + let pressed = input.pressed(TKeyDefinition::get_key()); - let MouseWorldPosition(Some(mouse_position)) = *mouse_world_position else { + let MouseWorldPosition(Some(position)) = *mouse_world_position else { return; }; - for (mut clickable, collider, transform) in &mut entities { - let Some(collider) = colliders.get(collider.get_handle()) else { - continue; - }; - let relative_mouse_position = Relative::position(mouse_position).to(transform); - - if clickable.clicked == collider.is_point_hit(relative_mouse_position) { - continue; - }; - - clickable.clicked = !clickable.clicked; + for (clickable, collider, transform) in &mut entities { + match pressed { + false => Self::set_not_clicked(clickable), + true => Self::set_clicked(clickable, collider, transform, &colliders, position), + } } } @@ -156,7 +178,7 @@ mod test_update { enum _Device { Pressed(Option), Held(Option), - Rest, + Released, } fn setup( @@ -177,7 +199,7 @@ mod test_update { mouse_input.clear_just_pressed(_DeviceKey); mouse_position } - _Device::Rest => Some(Vec2::default()), + _Device::Released => Some(Vec2::default()), }); assets.insert(handle, collider_asset); @@ -316,19 +338,19 @@ mod test_update { } #[test] - fn do_nothing_if_not_mouse_right_clicked() -> Result<(), RunSystemError> { + fn set_not_clicked_when_released() -> Result<(), RunSystemError> { let asset = _ColliderAsset { mock: new_mock!(Mock_ColliderAsset, |mock| { mock.expect_is_point_hit().never().return_const(true); }), }; let handle = new_handle!(_ColliderAsset); - let mut app = setup(&handle, asset, _Device::Rest); + let mut app = setup(&handle, asset, _Device::Released); let entity = app .world_mut() .spawn(( Clickable::<_Button> { - clicked: false, + clicked: true, ..default() }, _Collider(handle), @@ -394,7 +416,7 @@ mod test_update { } #[test] - fn do_not_mut_deref_clickable_when_nothing_changed() { + fn do_not_mut_deref_clickable_when_nothing_changed_on_hold() { let asset = _ColliderAsset { mock: new_mock!(Mock_ColliderAsset, |mock| { mock.expect_is_point_hit().return_const(true); @@ -429,6 +451,43 @@ mod test_update { app.world().entity(entity).get::<_Changed>(), ); } + + #[test] + fn do_not_mut_deref_clickable_when_nothing_changed_on_released() { + let asset = _ColliderAsset { + mock: new_mock!(Mock_ColliderAsset, |mock| { + mock.expect_is_point_hit().return_const(true); + }), + }; + let handle = new_handle!(_ColliderAsset); + let mut app = setup(&handle, asset, _Device::Released); + let entity = app + .world_mut() + .spawn(( + Clickable::<_Button> { + clicked: false, + ..default() + }, + _Collider(handle), + )) + .id(); + + app.add_systems( + Update, + ( + Clickable::<_Button>::detect_click_on::<_Collider>, + _Changed::detect, + ) + .chain(), + ); + app.update(); + app.update(); + + assert_eq!( + Some(&_Changed(false)), + app.world().entity(entity).get::<_Changed>(), + ); + } } #[cfg(test)] From cd332c9a6027e233bbe1aebb58d27d3c07004698 Mon Sep 17 00:00:00 2001 From: Codaishin Date: Sat, 18 Jan 2025 01:00:35 +0100 Subject: [PATCH 5/5] feat: switch on a single start --- src/components/clickable.rs | 271 +++++++++++++++++++++++++++++++++++- src/main.rs | 2 +- 2 files changed, 267 insertions(+), 6 deletions(-) diff --git a/src/components/clickable.rs b/src/components/clickable.rs index 6a789a5..deda96a 100644 --- a/src/components/clickable.rs +++ b/src/components/clickable.rs @@ -78,20 +78,51 @@ where } pub fn toggle( - toggle_state: TComponent, + toggle_on: TComponent, ) -> impl Fn(Query<(&Self, &mut TComponent), Changed>) where TComponent: Component + Default + PartialEq + Copy, { - move |mut entities| { - for (Self { clicked, .. }, mut component) in &mut entities { + move |mut toggles| { + for (Self { clicked, .. }, mut toggle) in &mut toggles { if !clicked { continue; } - *component = match *component == toggle_state { + *toggle = match *toggle == toggle_on { true => TComponent::default(), - false => toggle_state, + false => toggle_on, + } + } + } + } + + fn just_clicked(clickable: &Ref) -> bool { + clickable.is_changed() && clickable.clicked + } + + fn only_others_clicked(clickable: &Ref, any_clicked: bool) -> bool { + any_clicked && !clickable.clicked + } + + pub fn switch_on_single( + switch_on_state: TComponent, + ) -> impl Fn(Query<(Ref, &mut TComponent)>) + where + TComponent: Component + Default + PartialEq + Copy, + { + let switched_on = move |switch: &TComponent| switch == &switch_on_state; + + move |mut switches| { + let any_clicked = switches.iter().any(|(clickable, _)| clickable.clicked); + + for (clickable, mut switch) in &mut switches { + if Self::just_clicked(&clickable) { + *switch = switch_on_state + } + + if Self::only_others_clicked(&clickable, any_clicked) && switched_on(&switch) { + *switch = TComponent::default() } } } @@ -647,3 +678,233 @@ mod test_toggle { ); } } + +#[cfg(test)] +mod test_switch_on_single { + use super::*; + use crate::test_tools::SingleThreaded; + use std::ops::DerefMut; + + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] + struct _DeviceKey; + + #[derive(Debug, PartialEq, Default)] + struct _Button; + + impl GetKey for _Button { + type TKey = _DeviceKey; + + fn get_key() -> Self::TKey { + _DeviceKey + } + } + + #[derive(Component, Debug, PartialEq, Default, Clone, Copy)] + enum _Component { + #[default] + SwitchedOff, + SwitchedOn, + OtherState, + } + + fn setup() -> App { + let mut app = App::new().single_threaded(Update); + app.add_systems( + Update, + Clickable::<_Button>::switch_on_single(_Component::SwitchedOn), + ); + + app + } + + #[test] + fn switch_component_on() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )) + .id(); + + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOn), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn switch_component_off_if_new_switched_on() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOn, + Clickable::<_Button> { + clicked: false, + ..default() + }, + )) + .id(); + app.world_mut().spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )); + + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOff), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn do_not_switch_component_off_if_not_on_and_new_switched_on() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::OtherState, + Clickable::<_Button> { + clicked: false, + ..default() + }, + )) + .id(); + app.world_mut().spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )); + + app.update(); + + assert_eq!( + Some(&_Component::OtherState), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn do_not_switch_component_off_if_no_other_switched_on() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOn, + Clickable::<_Button> { + clicked: false, + ..default() + }, + )) + .id(); + + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOn), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn switch_component_on_only_once() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )) + .id(); + + app.update(); + app.world_mut() + .entity_mut(entity) + .insert(_Component::SwitchedOff); + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOff), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn switch_component_on_again_after_mut_deref() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )) + .id(); + + app.update(); + app.world_mut() + .entity_mut(entity) + .get_mut::>() + .unwrap() + .deref_mut(); + app.world_mut() + .entity_mut(entity) + .insert(_Component::SwitchedOff); + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOn), + app.world().entity(entity).get::<_Component>(), + ); + } + + #[test] + fn switch_component_off_if_new_switched_on_in_later_frame() { + let mut app = setup(); + let entity = app + .world_mut() + .spawn(( + _Component::SwitchedOn, + Clickable::<_Button> { + clicked: false, + ..default() + }, + )) + .id(); + + app.update(); + + app.world_mut().spawn(( + _Component::SwitchedOff, + Clickable::<_Button> { + clicked: true, + ..default() + }, + )); + + app.update(); + + assert_eq!( + Some(&_Component::SwitchedOff), + app.world().entity(entity).get::<_Component>(), + ); + } +} diff --git a/src/main.rs b/src/main.rs index ef06952..b25264c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,7 +50,7 @@ fn main() -> AppExit { Update, ( Clickable::::toggle(TileType::Obstacle), - Clickable::::toggle(TileType::Start), + Clickable::::switch_on_single(TileType::Start), TileType::update_color, ) .chain(),