diff --git a/assets/sprites/interactive/stairs.png b/assets/sprites/interactive/stairs.png index 3915c93..b236306 100644 Binary files a/assets/sprites/interactive/stairs.png and b/assets/sprites/interactive/stairs.png differ diff --git a/src/layout/asset/mod.rs b/src/layout/asset/mod.rs index 4b2c66a..213f666 100644 --- a/src/layout/asset/mod.rs +++ b/src/layout/asset/mod.rs @@ -62,9 +62,13 @@ pub fn load_level ( for DoorData { pos, facing_left, lock } in level.interactive.doors.iter() { let door_pos = vec2(meters(pos.x + 0.5), meters(pos.y)); - let mut door = parent.spawn(door_bundle(&textures, door_pos, *facing_left)); + let door_id = parent.spawn(door_bundle(&textures, door_pos, *facing_left)) + .observe(super::door::on_door_interact) + .id(); if let Some(lock_facing_left) = lock { - door.with_child(padlock_bundle(&textures, *lock_facing_left)); + parent.commands().spawn(padlock_bundle(&textures, *lock_facing_left)) + .observe(super::lock::on_padlock_interaction) + .insert(ChildOf(door_id)); } } @@ -89,6 +93,7 @@ pub fn load_level ( let pos = vec2(meters(pos.x), meters(pos.y)); let mut container = parent.spawn(container_bundle(&textures, pos, *size)); + container.observe(super::container::on_container_interact); for item in items { // TODO: replace with proper item-by-id system diff --git a/src/layout/container.rs b/src/layout/container.rs index 953358a..9cdeb71 100644 --- a/src/layout/container.rs +++ b/src/layout/container.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use bevy::{ecs::relationship::RelatedSpawner, prelude::*}; use bevy_rapier2d::prelude::*; use crate::{ @@ -35,6 +35,54 @@ pub fn on_container_interact( next_state.set(GameState::Inventory); } +pub fn on_container_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_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);