diff --git a/Cargo.lock b/Cargo.lock index ae1bfa5..6531503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "bevy_light_2d" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d40446e54d26895cdf32a5c6c0f5dd3104bbdeff53fc392ec85fb2d164fa51a" +dependencies = [ + "bevy", + "smallvec", +] + [[package]] name = "bevy_log" version = "0.18.0" @@ -2523,6 +2533,7 @@ dependencies = [ "bevy", "bevy_common_assets", "bevy_input", + "bevy_light_2d", "bevy_rapier2d", "clap", "leafwing-input-manager", diff --git a/Cargo.toml b/Cargo.toml index 11662b9..e3ab1e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2024" bevy = { version = "0.18.0", features = ["bevy_remote", "debug", "experimental_bevy_ui_widgets"] } bevy_common_assets = { version = "0.15.0", features = ["toml"] } bevy_input = { version = "0.18.0", features = ["serde", "serialize"] } +bevy_light_2d = "0.9.0" bevy_rapier2d = { version = "0.33.0", features = ["rapier-debug-render"] } clap = { version = "4.6.0", features = ["derive"] } leafwing-input-manager = "0.20.0" diff --git a/assets/levels/level.toml b/assets/levels/level.toml index b432e2b..fe3c8ca 100644 --- a/assets/levels/level.toml +++ b/assets/levels/level.toml @@ -111,3 +111,13 @@ h = 2 [[interactive.containers.items]] id = "lockpick" rotated = true + +[[interactive.lamps]] +x = 8 +y = 3 + +[[interactive.lamps]] +x = 8 +y = 7 +intensity = 3 +radius = 6 diff --git a/assets/levels/level_alt.toml b/assets/levels/level_alt.toml index 35d45c1..d9064ca 100644 --- a/assets/levels/level_alt.toml +++ b/assets/levels/level_alt.toml @@ -52,3 +52,8 @@ containers = [ ], }, ] +lamps = [ + { x = 8, y = 3 }, + { x = 8, y = 7, intensity = 3, radius = 16 }, + { x = 8, y = 11, intensity = 2, radius = 16 }, +] diff --git a/src/layout/asset/mod.rs b/src/layout/asset/mod.rs index 187361f..4a355a2 100644 --- a/src/layout/asset/mod.rs +++ b/src/layout/asset/mod.rs @@ -3,20 +3,9 @@ use std::collections::HashMap; use bevy::prelude::*; use crate::{ - LoadingState, - meters, - item::lockpick::lockpick_bundle, - layout::{ - Level, - LevelAssetHandle, - asset::structs::*, - container::container_bundle, - door::door_bundle, - lock::padlock_bundle, - stairs::stairs_bundle, - tilemap::tilemap_bundle - }, - player::player_bundle, + LoadingState, item::lockpick::lockpick_bundle, layout::{ + Level, LevelAssetHandle, asset::structs::*, container::container_bundle, door::door_bundle, light::lamp_bundle, lock::padlock_bundle, stairs::stairs_bundle, tilemap::tilemap_bundle + }, meters, player::player_bundle }; pub mod structs; @@ -107,5 +96,10 @@ pub fn load_level ( } } } + + for LampData { pos, intensity, radius } in level.interactive.lamps.iter() { + let pos = vec2(meters(pos.x), meters(pos.y - 0.5)); + parent.spawn(lamp_bundle(&asset_server, pos, *intensity, meters(*radius))); + } }); } diff --git a/src/layout/asset/structs/inner.rs b/src/layout/asset/structs/inner.rs index 7ac7dc8..f30944a 100644 --- a/src/layout/asset/structs/inner.rs +++ b/src/layout/asset/structs/inner.rs @@ -176,6 +176,31 @@ impl From for ContainerData { } } +pub(super) fn default_intensity() -> f32 { 2. } + +pub(super) fn default_radius() -> f32 { 4. } + +#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)] +#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)] +pub(super) struct LampDataInner { + #[serde(flatten)] + pub pos: Pos, + #[serde(default = "default_intensity")] + pub intensity: f32, + #[serde(default = "default_radius")] + pub radius: f32, +} + +impl From for LampData { + fn from(LampDataInner { pos, intensity, radius }: LampDataInner) -> Self { + Self { + pos: pos.into(), + intensity, + radius, + } + } +} + #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)] #[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)] pub(super) struct InteractiveInner { @@ -186,15 +211,18 @@ pub(super) struct InteractiveInner { pub stairs: Option>, #[serde(default)] pub containers: Option>, + #[serde(default)] + pub lamps: Option>, } impl From for Interactive { - fn from(InteractiveInner { player, doors, stairs, containers }: InteractiveInner) -> Self { + fn from(InteractiveInner { player, doors, stairs, containers, lamps }: InteractiveInner) -> Self { Self { player: player.into(), doors: doors.unwrap_or_default(), stairs: stairs.unwrap_or_default(), containers: containers.unwrap_or_default(), + lamps: lamps.unwrap_or_default(), } } } diff --git a/src/layout/asset/structs/mod.rs b/src/layout/asset/structs/mod.rs index 4cba5b0..d85a6af 100644 --- a/src/layout/asset/structs/mod.rs +++ b/src/layout/asset/structs/mod.rs @@ -67,6 +67,15 @@ pub struct ContainerData { pub items: Vec, } +#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)] +#[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)] +#[serde(from = "inner::LampDataInner")] +pub struct LampData { + pub pos: Vec2, + pub intensity: f32, + pub radius: f32, +} + #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone, Reflect)] #[reflect(Debug, Default, Deserialize, Serialize, PartialEq, Clone)] #[serde(from = "inner::InteractiveInner")] @@ -78,6 +87,8 @@ pub struct Interactive { pub stairs: Vec, #[serde(default)] pub containers: Vec, + #[serde(default)] + pub lamps: Vec, } #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Deref, DerefMut, Reflect)] diff --git a/src/layout/door.rs b/src/layout/door.rs index 1dc5fe2..376969a 100644 --- a/src/layout/door.rs +++ b/src/layout/door.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_light_2d::prelude::*; use bevy_rapier2d::prelude::*; use crate::meters; @@ -13,6 +14,11 @@ const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png"; #[require(Sprite, InteractiveObject)] pub struct Door(pub i8); +#[derive(Component, Clone, Copy, Reflect, Default, PartialEq, Eq, Debug)] +#[reflect(Component, Clone, Default, PartialEq, Debug)] +#[require(Collider, LightOccluder2d)] +pub struct DoorCollider; + impl Default for Door { fn default() -> Self { Self(1) @@ -23,26 +29,22 @@ pub fn on_door_interact( event: On, mut commands: Commands, locked_query: Query<(), With>, - collider_query: Query<(), (With, With)>, - no_collider_query: Query<(), (With, Without)>, door_query: Query<(&Door, &Children)>, + door_collider_query: Query>, mut sprite_query: Query<(&mut Sprite, &mut Transform)>, asset_server: Res, ) { if locked_query.get(event.entity).is_ok() { return; } - let was_opened = if collider_query.get(event.entity).is_ok() { false } - else if no_collider_query.get(event.entity).is_ok() { true } - else { - return; - }; let Ok((door, children)) = door_query.get(event.entity) else { return; }; + let maybe_door_collider = children.iter() + .find_map(|id| if let Ok(id) = door_collider_query.get(id) { Some(id) } else { None } ); for child in children { if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) { - let (image, translation) = if was_opened { (DOOR_CLOSED_ASSET, 0.) } + let (image, translation) = if maybe_door_collider.is_none() { (DOOR_CLOSED_ASSET, 0.) } else { (DOOR_OPENED_ASSET, door.0 as f32 * meters(0.5)) }; sprite.image = asset_server.load(image); transform.translation.x = translation; @@ -50,15 +52,20 @@ pub fn on_door_interact( } } - if was_opened { - commands.entity(event.entity).insert(door_collider()); + if let Some(id) = maybe_door_collider { + commands.entity(id).despawn(); } else { - commands.entity(event.entity).remove::(); + commands.entity(event.entity).with_child(door_collider_bundle()); } } -fn door_collider() -> Collider { - Collider::cuboid(meters(0.06125), meters(1.)) +pub fn door_collider_bundle() -> impl Bundle { + let size = vec2(meters(0.06125), meters(1.)); + ( + DoorCollider, + Collider::cuboid(size.x, size.y), + LightOccluder2d { shape: LightOccluder2dShape::Rectangle { half_size: size } }, + ) } pub fn door_bundle(asset_server: &Res, position: Vec2, facing_left: bool) -> impl Bundle { @@ -68,8 +75,8 @@ pub fn door_bundle(asset_server: &Res, position: Vec2, facing_left: Door(direction), Transform::from_xyz(position.x, position.y, 0.), Name::new(format!("Door ({}, {})", position.x, position.y)), - door_collider(), children![ + door_collider_bundle(), ( Collider::cuboid(meters(0.5), meters(1.)), Sensor, diff --git a/src/layout/light.rs b/src/layout/light.rs new file mode 100644 index 0000000..9516dbd --- /dev/null +++ b/src/layout/light.rs @@ -0,0 +1,26 @@ +use bevy::prelude::*; +use bevy_light_2d::prelude::*; + +use crate::meters; + +const LAMP_IMAGE_PATH: &'static str = "sprites/interactive/lamp.png"; + +#[derive(Component, Clone, Copy, Default, Reflect, Debug, PartialEq, Eq)] +#[reflect(Component, Clone, Default, Debug, PartialEq)] +#[require(Transform)] +pub struct Lamp; + +pub fn lamp_bundle(asset_server: &Res, pos: Vec2, intensity: f32, radius: f32) -> impl Bundle { + let image = asset_server.load(LAMP_IMAGE_PATH); + ( + SpotLight2d { + intensity, + radius, + source_width: meters(0.5), + cast_shadows: true, + ..default() + }, + Transform::from_xyz(pos.x, pos.y, 0.), + Sprite::from_image(image), + ) +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 654ee4d..645e79b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -3,6 +3,7 @@ use bevy::prelude::*; pub mod asset; pub mod container; pub mod door; +pub mod light; pub mod lock; pub mod stairs; pub mod systems; diff --git a/src/layout/tilemap.rs b/src/layout/tilemap.rs index 1e81175..aa1da05 100644 --- a/src/layout/tilemap.rs +++ b/src/layout/tilemap.rs @@ -10,6 +10,7 @@ use bevy::{ TilemapChunkTileData, }, }; +use bevy_light_2d::prelude::*; use bevy_rapier2d::prelude::*; use crate::meters; @@ -58,10 +59,13 @@ pub fn tilemap_bundle( let mut rect = rect.as_rect(); rect.max = vec2(rect.max.x + 1., rect.max.y + 1.); - let (width, height) = (rect.width(), rect.height()); + let (width, height) = (meters(rect.width() * 0.5), meters(rect.height() * 0.5)); let offset = rect.center(); ( - Collider::cuboid(meters(width * 0.5), meters(height * 0.5)), + Collider::cuboid(width, height), + LightOccluder2d { + shape: LightOccluder2dShape::Rectangle { half_size: vec2(width, height) }, + }, Transform::from_xyz( meters(offset.x - size.x as f32 * 0.5), meters(offset.y - size.y as f32 * 0.5), diff --git a/src/lib.rs b/src/lib.rs index 3ee81a7..b4be247 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use bevy_rapier2d::{ prelude::*, rapier::prelude::IntegrationParameters, }; +use bevy_light_2d::prelude::*; use clap::Parser; pub mod input; @@ -60,6 +61,9 @@ pub fn camera_bundle() -> impl Bundle { scale: 1., ..OrthographicProjection::default_2d() }), + Light2d { + ambient_light: AmbientLight2d { brightness: 0.25, ..default() } + }, Name::new("Camera2d"), ) } @@ -101,6 +105,7 @@ impl Plugin for ExpeditionPlugin { TomlAssetPlugin::::new(&["toml"]), input::plugin::InputAssetPlugin::::default(), input::plugin::InputAssetPlugin::::default(), + Light2dPlugin, )) .init_state::() .init_state::()