From 1f9dace4ce3e16148ede03076c493ba8f97bcb9c Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 1 Apr 2026 15:29:47 +0300 Subject: [PATCH] feat: Interaction highlighting - Added Collision{Started,Stopped}Event - Added player headlight just for showcasing --- assets/sprites/interactive/stairs.png | Bin 896 -> 875 bytes src/layout/asset/mod.rs | 9 ++- src/layout/container.rs | 64 ++++++++++++++++--- src/layout/door.rs | 85 ++++++++++++++++++++------ src/layout/lock.rs | 65 +++++++++++++++++--- src/layout/mod.rs | 15 +++++ src/layout/stairs.rs | 47 ++++++++++++-- src/layout/systems.rs | 27 +++++--- src/lib.rs | 5 +- src/player/mod.rs | 14 +++++ src/player/systems.rs | 26 ++++++-- 11 files changed, 303 insertions(+), 54 deletions(-) diff --git a/assets/sprites/interactive/stairs.png b/assets/sprites/interactive/stairs.png index 3915c9304ba3c739943555d7d16eef4545b39e1c..b23630631ef0bd50dfa83a3c94990abffb8ab58e 100644 GIT binary patch delta 839 zcmV-N1GxNv2kQoqF@GgVL_t(|ob8#virYXChewfK;u;h327(Y!?{JVdkm^Wrh1+|9 zRC$AIuSgNDGNcUzyElY@-@%QZ!YDQvo+d_ViUpFaO` zzR&(FW=oZ3#2!|$C|9cHmQO5H{N1s7`vCv|HUL&pAte-w3PE4Ke(PVA0HgAo(nu*$ zA;^`G^==EN-F8SBH!1|VFk()-?UQ31kPGrR+4H3HV1Fq_7KbR71i4BGCu1tMR8bK{ z5cg7y6BN>)rU;HAh+`QR>p>yUGSDx>w{d^dKXCRN8`br@H$f-(@%?ACA7hY8iw#xF zymVS@L=p6NCv3;pqcn;!1Pk(&=yN5gsf=8(m)JNh9DA7%SOj4S`}WTRDfDHzzT)@= z^C1;Gi_7>jWc+TQ+e2$o@ zxDrO)!zu{8x%2S=W!(fN$c1kkLEXcmm6#DTrw3^!dXvUds8YoV=`zoes(o7yd#cMR zy`Ny&C{m{?smZD>Rhkj|y90}IrAmAsBzpCp@qdHsSX2@_=@7<~)ivPq^EXmTR0ztG z)c}&8tooYl)Mu$YS-pV8O;)*=;yhWkAUauvHY5RMo~*jalT}8f(_$k}Rz2j&s)syT z^^hm49`a<>L!PX9$dgqMilgq4002ovPDHLkV1htNh-v@; delta 860 zcmV-i1Ec)w27m{UF@HKqL_t(|ob8#hZrd;rhEJ7VB8w694FnVgw7Uq9wLqt!Lk6#T zflhq`uNgWNo(yCyP}sYHqJZDQi=M(zhw4;@VMtlz-H|H4$x#y7`taY8^c9dWtETRJ z@9}sz7WlgBx()!KD2m})<@*K_pqR(Qu~;>Ar#|GA`#>+}?tkGlyhcgM2vQ$rs*u-E z73X(1JqniPE%11dqBs}ME81f`aiu~Z?>Gmx+pegC6hufX%+ z@GO*SSrtW5n17BMoAm;M9yL%*ilwGYLODRuq*W;?r3xhh=hqz^soJ+CyuE%msgLg4 z=Rd1r5SEQ1b*k<^f1LckU%vjf{+|6TW=d7eh#gi{QMOb~EuT{i%Od0%+x@H&Sk*5M)cpa<_%kZabumn|~+-*+4O;-S)*b4#)+$o9ubi zeN-tZi$fGkf@~$ECSxq7R8bK`5cg87Cdj8hjS(C~5XUks)`NV`GSDx>_hCQNGdTN= ziR$L|z1IzX{`eK_!yr;=F%i`=FP#<>K?MEV37c{Cs2If{-hx~u`dSIbR7N(~OH5A- z$6m$<7Josignj$>Kni_XF0VK~!E{IkosS1tUICPKGhDl=U;a(Z1Q(<}uqwueZ?80R*H(P(NpZRE};q)@hM`)VoMlx539iQ;m*ecly&2kARDf21a%J!R$?e-N)L*e z;7wYULX|2`NSAqzRPEbx*i&6j>HG_pjUsibW`8wVHKi(M#J=uOMcGm%z7G<8_a6P= zs*9S%PTB{3vbqGd{M?O{k|+e_$*KplpRBr??AWtZo~&L#a+6i=r8rMk4Tw%wq4h~X znJ23@@?@0}>9m;0lT`)VsJ2tmWnT2%Mh4|nwcEksV mv6Gv$gCr-boFp?@mEsR0LhMAA3qN@P0000, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + highlight_sprite_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if highlight_sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Visible); + break; + } + } + + commands.entity(parent_id.0).insert(Visibility::Hidden); +} + +pub fn on_container_collision_stopped( + event: On, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + highlight_sprite_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if highlight_sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Hidden); + break; + } + } + + commands.entity(parent_id.0).insert(Visibility::Visible); +} + pub fn container_bundle( textures: &Res, position: Vec2, @@ -48,14 +96,16 @@ pub fn container_bundle( sprite, Inventory::new(inventory_size), Children::spawn(( - Spawn(( - Collider::cuboid(meters(1.), meters(1.)), - Sensor, - Transform::from_xyz(0., meters(0.5), 0.), - )), + SpawnWith(|parent: &mut RelatedSpawner| { + parent.spawn(( + Collider::cuboid(meters(1.), meters(1.)), + Sensor, + Transform::from_xyz(0., meters(0.5), 0.), + )).observe(on_container_collision_started) + .observe(on_container_collision_stopped); + }), Spawn(( highlight_sprite, - Transform::from_xyz(0., 0., 1.), Visibility::Hidden, )), )), diff --git a/src/layout/door.rs b/src/layout/door.rs index 7f436ee..f39d83f 100644 --- a/src/layout/door.rs +++ b/src/layout/door.rs @@ -1,8 +1,8 @@ -use bevy::prelude::*; +use bevy::{ecs::relationship::RelatedSpawner, prelude::*}; use bevy_light_2d::prelude::*; use bevy_rapier2d::prelude::*; -use crate::meters; +use crate::{layout::lock::Padlock, meters}; use super::*; @@ -61,7 +61,8 @@ pub fn on_door_interact( }, Err(_) => continue, } - let texture = if maybe_door_collider.is_none() { &textures.door_closed } else { &textures.door_opened }; + let texture = if maybe_door_collider.is_none() { &textures.door_closed } + else { &textures.door_opened }; let (sprite, highlight_sprite) = door_sprites(texture, door.is_facing_left()); let needed_sprite = if highlight_query.get(*child).is_err() { sprite } else { highlight_sprite }; commands.entity(*child).insert(needed_sprite); @@ -74,6 +75,56 @@ pub fn on_door_interact( } } +pub fn on_door_collision_started( + event: On, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + sprite_query: Query<(), (With, Without)>, + highlight_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if sprite_query.get(*child).is_ok() { + match highlight_query.get(*child) { + Ok(_) => { commands.entity(*child).insert(Visibility::Visible); }, + Err(_) => { commands.entity(*child).insert(Visibility::Hidden); }, + } + } + } +} + +pub fn on_door_collision_stopped( + event: On, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + sprite_query: Query<(), (With, Without)>, + highlight_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if sprite_query.get(*child).is_ok() { + match highlight_query.get(*child) { + Ok(_) => { commands.entity(*child).insert(Visibility::Hidden); }, + Err(_) => { commands.entity(*child).insert(Visibility::Visible); }, + } + } + } +} + pub fn door_collider_bundle() -> impl Bundle { let size = vec2(meters(0.06125), meters(1.)); ( @@ -103,22 +154,22 @@ pub fn door_bundle(textures: &Res, position: Vec2, facing_left: Transform::from_xyz(position.x, position.y, 0.), Name::new(format!("Door ({}, {})", position.x, position.y)), InheritedVisibility::VISIBLE, - children![ - door_collider_bundle(), - ( - Collider::cuboid(meters(0.5), meters(1.)), - Transform::default(), - Sensor, - ), - ( - sprite, - ), - ( + Children::spawn(( + Spawn(door_collider_bundle()), + SpawnWith(|parent: &mut RelatedSpawner| { + parent.spawn(( + Collider::cuboid(meters(0.5), meters(1.)), + Transform::default(), + Sensor, + )).observe(on_door_collision_started) + .observe(on_door_collision_stopped); + }), + Spawn(sprite), + Spawn(( highlight_sprite, - Transform::from_xyz(0., 0., 1.), DoorHighlight, Visibility::Hidden, - ) - ], + )), + )), ) } diff --git a/src/layout/lock.rs b/src/layout/lock.rs index 23de591..521daed 100644 --- a/src/layout/lock.rs +++ b/src/layout/lock.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use bevy::{ecs::relationship::RelatedSpawner, prelude::*}; use bevy_rapier2d::prelude::*; use crate::{ @@ -49,6 +49,54 @@ pub fn on_padlock_interaction( commands.entity(lockpick_id).despawn(); } +pub fn on_padlock_collision_started( + event: On, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + highlight_sprite_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if highlight_sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Visible); + break; + } + } + + commands.entity(parent_id.0).insert(Visibility::Hidden); +} + +pub fn on_padlock_collision_stopped( + event: On, + mut commands: Commands, + collider_query: Query<&ChildOf, With>, + parent_query: Query<&Children>, + highlight_sprite_query: Query<(), With>, +) { + let Ok(parent_id) = collider_query.get(event.entity) else { + return; + }; + let Ok(children) = parent_query.get(parent_id.0) else { + return; + }; + + for child in children { + if highlight_sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Hidden); + break; + } + } + + commands.entity(parent_id.0).insert(Visibility::Visible); +} + pub fn padlock_bundle(textures: &Res, facing_left: bool) -> impl Bundle { let sign = if facing_left { -1. } else { 1. }; ( @@ -57,14 +105,17 @@ pub fn padlock_bundle(textures: &Res, facing_left: bool) -> impl flip_x: !facing_left, ..textures.lock.sprite("main") }, - Transform::from_xyz(meters(sign * 0.125), meters(0.), 0.), + Transform::from_xyz(meters(sign * 0.125), meters(0.), 1.), InheritedVisibility::VISIBLE, Children::spawn(( - Spawn(( - Transform::from_xyz(meters(sign * 0.1875), 0., 0.), - Collider::cuboid(meters(0.1875), meters(1.)), - Sensor, - )), + SpawnWith(move |parent: &mut RelatedSpawner| { + parent.spawn(( + Transform::from_xyz(meters(sign * 0.1875), 0., 0.), + Collider::cuboid(meters(0.1875), meters(1.)), + Sensor, + )).observe(on_padlock_collision_started) + .observe(on_padlock_collision_stopped); + }), Spawn(( Sprite { flip_x: !facing_left, diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 4880c73..843d210 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -35,6 +35,21 @@ pub struct LevelAssetHandle(Handle); #[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)] #[reflect(Event, Debug, PartialEq, Clone)] pub struct InteractionEvent { + #[event_target] + pub entity: Entity, +} + +#[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)] +#[reflect(Event, Debug, PartialEq, Clone)] +pub struct CollisionStartedEvent { + #[event_target] + pub entity: Entity, +} + +#[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)] +#[reflect(Event, Debug, PartialEq, Clone)] +pub struct CollisionStoppedEvent { + #[event_target] pub entity: Entity, } diff --git a/src/layout/stairs.rs b/src/layout/stairs.rs index 6e6441c..054d88e 100644 --- a/src/layout/stairs.rs +++ b/src/layout/stairs.rs @@ -60,6 +60,40 @@ pub fn on_stairs_interact( player_transform.translation.y += offset.y; } +pub fn on_stairs_collision_started( + event: On, + mut commands: Commands, + collider_query: Query<&Children, With>, + sprite_query: Query<(), With>, +) { + let Ok(children) = collider_query.get(event.entity) else { + return; + }; + + for child in children { + if sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Visible); + } + } +} + +pub fn on_stairs_collision_stopped( + event: On, + mut commands: Commands, + collider_query: Query<&Children, With>, + sprite_query: Query<(), With>, +) { + let Ok(children) = collider_query.get(event.entity) else { + return; + }; + + for child in children { + if sprite_query.get(*child).is_ok() { + commands.entity(*child).insert(Visibility::Hidden); + } + } +} + pub fn stairs_bundle( textures: &Res, position: Vec2, @@ -89,10 +123,11 @@ pub fn stairs_bundle( Visibility::Hidden, )), ), - )); + )).observe(on_stairs_collision_started) + .observe(on_stairs_collision_stopped); parent.spawn(( stairs.sprite("main"), - Transform::from_xyz(0., meters(1.5), 0.), + Transform::from_xyz(0., meters(1.5), -1.), )); } @@ -104,11 +139,15 @@ pub fn stairs_bundle( InheritedVisibility::VISIBLE, Children::spawn( Spawn(( - stairs.sprite("down"), + Sprite { + flip_y: true, + ..stairs.sprite("up") + }, Visibility::Hidden, )), ), - )); + )).observe(on_stairs_collision_started) + .observe(on_stairs_collision_stopped); } }), )), diff --git a/src/layout/systems.rs b/src/layout/systems.rs index 505b16f..d3e780f 100644 --- a/src/layout/systems.rs +++ b/src/layout/systems.rs @@ -54,19 +54,35 @@ pub fn detect_interact_collisions( for collision_event in collision_events.read() { match collision_event { CollisionEvent::Started(first, second, _) => { - if let Some(interactive_id) = interact_collisions_inner(*first, *second, interactive_query1, player_query, parent_query) { + if let Some(interactive_id) = interact_collisions_inner( + *first, + *second, + interactive_query1, + player_query, + parent_query + ) { commands.entity(interactive_id).insert(MayInteract); + commands.trigger(CollisionStartedEvent { entity: *first }); } - if let Some(interactive_id) = interact_collisions_inner(*second, *first, interactive_query1, player_query, parent_query) { + if let Some(interactive_id) = interact_collisions_inner( + *second, + *first, + interactive_query1, + player_query, + parent_query + ) { commands.entity(interactive_id).insert(MayInteract); + commands.trigger(CollisionStartedEvent { entity: *second }); } }, CollisionEvent::Stopped(first, second, _) => { if let Some(interactive_id) = interact_collisions_inner(*first, *second, interactive_query2, player_query, parent_query) { commands.entity(interactive_id).remove::(); + commands.trigger(CollisionStoppedEvent { entity: *first }); } if let Some(interactive_id) = interact_collisions_inner(*second, *first, interactive_query2, player_query, parent_query) { commands.entity(interactive_id).remove::(); + commands.trigger(CollisionStoppedEvent { entity: *second }); } }, } @@ -166,13 +182,6 @@ pub fn load_layout_textures( uvec2(72, 8), )), ); - indices.insert( - "down".to_owned(), - atlas.add_texture(URect::from_corners( - uvec2(64, 8), - uvec2(72, 16), - )), - ); atlases.add(atlas) }; textures.stairs = AtlasLayoutTexture::new(image, atlas, indices); diff --git a/src/lib.rs b/src/lib.rs index c087d58..fc0c1e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub fn camera_bundle() -> impl Bundle { ..OrthographicProjection::default_2d() }), Light2d { - ambient_light: AmbientLight2d { brightness: 0.25, ..default() } + ambient_light: AmbientLight2d { brightness: 0.1, ..default() } }, Name::new("Camera2d"), ) @@ -131,9 +131,6 @@ impl Plugin for ExpeditionPlugin { .add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory) .add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory) .add_observer(ui::inventory::observers::on_ui_rotate) - .add_observer(layout::container::on_container_interact) - .add_observer(layout::door::on_door_interact) - .add_observer(layout::lock::on_padlock_interaction) .add_observer(layout::stairs::on_stairs_interact); } } diff --git a/src/player/mod.rs b/src/player/mod.rs index 1e94631..a479c61 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_light_2d::prelude::*; use bevy_rapier2d::prelude::*; use crate::{ @@ -39,6 +40,19 @@ pub fn player_bundle(asset_server: &Res, position: Vec2) -> impl Bu Inventory::new(UVec2::new(5, 3)), Inventory::new(UVec2::new(4, 4)), camera_bundle(), + ( + SpotLight2d { + color: Color::linear_rgb(0.98, 0.98, 0.824), + intensity: 1., + radius: meters(6.), + source_width: 0., + cast_shadows: true, + falloff: meters(8.), + direction: 0., + ..default() + }, + Transform::from_xyz(meters(0.1875), meters(0.625), 0.), + ) ], ) } diff --git a/src/player/systems.rs b/src/player/systems.rs index 416debb..73d3fb2 100644 --- a/src/player/systems.rs +++ b/src/player/systems.rs @@ -17,14 +17,21 @@ pub fn handle_input( state: Res>, mut next_state: ResMut>, interactables: Query>, - mut player: Query<(&Player, &mut ActionState, &mut KinematicCharacterController, &mut Sprite)>, + mut player: Query<( + &Player, + &mut ActionState, + &mut KinematicCharacterController, + &mut Sprite, + &Children + )>, + mut player_light: Query<(&mut SpotLight2d, &mut Transform)>, ) { let Ok(player) = player.single_mut() else { return; }; match state.get() { GameState::Running => { - let (Player {speed}, mut action_state, mut controller, mut sprite) = player; + let (Player {speed}, mut action_state, mut controller, mut sprite, children) = player; if action_state.just_released(&Action::ToggleInventory) { next_state.set(GameState::Inventory); @@ -35,7 +42,18 @@ pub fn handle_input( controller.translation = Some(vec2(direction * speed * time.delta_secs(), 0.)); if direction != 0f32 { - sprite.flip_x = direction < 0f32; + let facing_left = direction < 0f32; + sprite.flip_x = facing_left; + for child in children { + if let Ok((mut spotlight, mut transform)) = player_light.get_mut(*child) { + spotlight.direction = if facing_left { 180. } else { 0. }; + let last_x = transform.translation.x; + if last_x < 0. && !facing_left || last_x > 0. && facing_left { + transform.translation.x = -transform.translation.x; + } + break; + } + } } if action_state.just_released(&Action::Interact) { @@ -46,7 +64,7 @@ pub fn handle_input( } }, GameState::Inventory => { - let (_, mut action_state, _, _) = player; + let (_, mut action_state, _, _, _) = player; if action_state.just_released(&Action::ToggleInventory) || action_state.just_released(&Action::Interact) { next_state.set(GameState::Running);