diff --git a/Cargo.toml b/Cargo.toml index 2253d6f..1f0d84d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["codegen-backend"] [package] name = "expedition_demo" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..5145335 --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; +use leafwing_input_manager::prelude::*; +use serde::{ + Serialize, + Deserialize +}; + +pub mod plugin; + +#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] +pub enum InputAction { + #[actionlike(Axis)] + Move, + ToggleInventory, + Interact, +} + +impl InputAction { + pub fn default_input_map() -> InputMap { + InputMap::default() + .with_axis(Self::Move, VirtualAxis::ad()) + .with_axis(Self::Move, GamepadAxis::LeftStickX) + .with(Self::ToggleInventory, KeyCode::KeyI) + .with(Self::ToggleInventory, GamepadButton::Select) + .with(Self::Interact, KeyCode::KeyE) + .with(Self::Interact, GamepadButton::East) + } +} + +#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] +pub enum UiAction { + Rotate, +} + +impl UiAction { + pub fn default_input_map() -> InputMap { + InputMap::default() + .with(Self::Rotate, KeyCode::KeyR) + .with(Self::Rotate, GamepadButton::West) + } +} diff --git a/src/input.rs b/src/input/plugin.rs similarity index 100% rename from src/input.rs rename to src/input/plugin.rs diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 91fd924..0cf99f7 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -1,7 +1,6 @@ use bevy::prelude::*; pub mod item; -pub mod ui; #[derive(Component, Reflect, Default)] pub struct Inventory { diff --git a/src/inventory/ui.rs b/src/inventory/ui.rs deleted file mode 100644 index acbd43c..0000000 --- a/src/inventory/ui.rs +++ /dev/null @@ -1 +0,0 @@ -pub use crate::ui::inventory::*; diff --git a/src/layout/container.rs b/src/layout/container.rs index e69de29..5236a26 100644 --- a/src/layout/container.rs +++ b/src/layout/container.rs @@ -0,0 +1,62 @@ +use bevy::prelude::*; +use bevy_rapier2d::prelude::*; + +use crate::{ + GameState, + PIXELS_PER_METER, + inventory::{ + ActiveInventory, + Inventory, + item::Item, + }, +}; + +const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png"; + +use super::*; + +#[derive(Component)] +#[require(Sprite, InteractiveObject, Inventory)] +pub struct Container; + +fn on_container_interact( + event: On, + mut commands: Commands, + locked_query: Query<(), With>, + crate_query: Query<(), With>, + mut next_state: ResMut>, +) { + if locked_query.get(event.entity).is_ok() { + return; + } + if crate_query.get(event.entity).is_err() { + return; + } + + commands.entity(event.entity).insert(ActiveInventory); + next_state.set(GameState::Inventory); +} + +pub fn container_bundle( + asset_server: &Res, + position: Vec2, + inventory_size: UVec2, + items: Vec +) -> impl Bundle { + let image = asset_server.load(CRATE_CLOSED_ASSET); + ( + Container, + Transform::from_xyz(position.x, position.y - PIXELS_PER_METER * 0.5, 0.), + Sprite::from_image(image), + Inventory::new(inventory_size), + Observer::new(on_container_interact), + Children::spawn(( + SpawnIter(items.into_iter()), + Spawn(( + Collider::cuboid(PIXELS_PER_METER, PIXELS_PER_METER), + Sensor, + Transform::from_xyz(0., PIXELS_PER_METER * 0.5, 0.), + )), + )), + ) +} diff --git a/src/layout/door.rs b/src/layout/door.rs index e69de29..fac9ea3 100644 --- a/src/layout/door.rs +++ b/src/layout/door.rs @@ -0,0 +1,82 @@ +use bevy::prelude::*; +use bevy_rapier2d::prelude::*; + +use crate::PIXELS_PER_METER; + +use super::*; + +const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png"; +const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png"; + +#[derive(Component)] +#[require(Sprite, InteractiveObject)] +pub struct Door(pub i8); + +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)>, + 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; + }; + 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.) } + else { (DOOR_OPENED_ASSET, door.0 as f32 * PIXELS_PER_METER * 0.5) }; + sprite.image = asset_server.load(image); + transform.translation.x = translation; + break; + } + } + + if was_opened { + commands.entity(event.entity).insert(door_collider()); + } else { + commands.entity(event.entity).remove::(); + } +} + +fn door_collider() -> Collider { + Collider::cuboid(PIXELS_PER_METER * 0.06125, PIXELS_PER_METER) +} + +pub fn door_bundle(asset_server: &Res, position: Vec2, facing_left: bool) -> impl Bundle { + let direction = if facing_left { -1 } else { 1 }; + let image = asset_server.load(DOOR_CLOSED_ASSET); + ( + Door(direction), + Transform::from_xyz(position.x, position.y, 0.), + Name::new(format!("Door ({}, {})", position.x, position.y)), + door_collider(), + Observer::new(on_door_interact), + children![ + ( + Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER), + Sensor, + Transform::from_xyz(0., 0., 0.), + ), + ( + Sprite { + image, + flip_x: facing_left, + ..default() + }, + Transform::from_xyz(0., 0., 0.), + ), + ], + ) +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9156229..3a7a3f9 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,33 +1,15 @@ -use bevy::{ecs::query::QueryFilter, prelude::*}; -use bevy_rapier2d::prelude::*; - -use crate::{GameState, PIXELS_PER_METER, inventory::{ActiveInventory, Inventory, item::Item}, player::Player}; +use bevy::prelude::*; pub mod container; pub mod door; pub mod systems; -const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png"; -const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png"; -const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png"; - #[derive(Component)] pub struct MayInteract; #[derive(Component, Default)] pub struct InteractiveObject; -#[derive(Component)] -pub struct Wall; - -#[derive(Component)] -#[require(Sprite, InteractiveObject)] -pub struct Door(pub i8); - -#[derive(Component)] -#[require(Sprite, InteractiveObject, Inventory)] -pub struct Crate; - #[derive(Component)] pub struct Locked; @@ -35,187 +17,3 @@ pub struct Locked; pub struct InteractionEvent { pub entity: Entity, } - -fn get_interactive_id( - tested_id: Entity, - interactive_query: Query<(), F>, - parent_query: Query<&ChildOf, With>, -) -> Option { - if interactive_query.get(tested_id).is_ok() { - return Some(tested_id); - } - let Ok(parent_id) = parent_query.get(tested_id) else { - return None; - }; - match interactive_query.get(parent_id.0) { - Ok(_) => Some(parent_id.0), - Err(_) => None, - } -} - -pub fn detect_interact_collisions( - mut commands: Commands, - mut collision_events: MessageReader, - player_query: Query<(), With>, - interactive_query1: Query<(), (With, Without)>, - interactive_query2: Query<(), (With, With)>, - parent_query: Query<&ChildOf, With>, -) { - for collision_event in collision_events.read() { - match collision_event { - CollisionEvent::Started(first, second, _) => { - let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else { - continue; - }; - if player_query.get(*second).is_err() { - continue; - } - commands.entity(interactive_id).insert(MayInteract); - }, - CollisionEvent::Stopped(first, second, _) => { - let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else { - continue; - }; - if player_query.get(*second).is_err() { - continue; - } - commands.entity(interactive_id).remove::(); - }, - } - } -} - -fn on_crate_interact( - event: On, - mut commands: Commands, - locked_query: Query<(), With>, - crate_query: Query<(), With>, - mut next_state: ResMut>, -) { - if locked_query.get(event.entity).is_ok() { - return; - } - if crate_query.get(event.entity).is_err() { - return; - } - - commands.entity(event.entity).insert(ActiveInventory); - next_state.set(GameState::Inventory); -} - -fn crate_bundle(image: Handle, position: Vec2, inventory_size: UVec2, items: Vec) -> impl Bundle { - ( - Crate, - Transform::from_xyz(position.x, position.y - PIXELS_PER_METER * 0.5, 0.), - Sprite::from_image(image), - Inventory::new(inventory_size), - Observer::new(on_crate_interact), - Children::spawn(( - SpawnIter(items.into_iter()), - Spawn(( - Collider::cuboid(PIXELS_PER_METER, PIXELS_PER_METER), - Sensor, - Transform::from_xyz(0., PIXELS_PER_METER * 0.5, 0.), - )), - )), - ) -} - -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)>, - 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; - }; - 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.) } - else { (DOOR_OPENED_ASSET, door.0 as f32 * PIXELS_PER_METER * 0.5) }; - sprite.image = asset_server.load(image); - transform.translation.x = translation; - break; - } - } - - if was_opened { - commands.entity(event.entity).insert(door_collider()); - } else { - commands.entity(event.entity).remove::(); - } -} - -fn door_collider() -> Collider { - Collider::cuboid(PIXELS_PER_METER * 0.06125, PIXELS_PER_METER) -} - -fn door_bundle(image: Handle, position: Vec2, facing_left: bool) -> impl Bundle { - let direction = if facing_left { -1 } else { 1 }; - ( - Door(direction), - Transform::from_xyz(position.x, position.y, 0.), - Name::new(format!("Door ({}, {})", position.x, position.y)), - door_collider(), - Observer::new(on_door_interact), - children![ - ( - Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER), - Sensor, - Transform::from_xyz(0., 0., 0.), - ), - ( - Sprite { - image, - flip_x: facing_left, - ..default() - }, - Transform::from_xyz(0., 0., 0.), - ), - ], - ) -} - -fn wall_bundle(position: Vec2) -> impl Bundle { - ( - Wall, - Transform::from_xyz(position.x, position.y, 0.), - Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER), - Name::new(format!("Wall ({}, {})", position.x, position.y)), - ) -} - -pub fn setup_world( - mut commands: Commands, - asset_server: Res, -) { - let items = vec![ - Item::new_positioned(uvec2(1, 1), uvec2(0, 0)), - Item::new_positioned(uvec2(2, 1), uvec2(1, 0)), - Item::new_positioned(uvec2(3, 1), uvec2(3, 0)), - Item::new_positioned(uvec2(2, 2), uvec2(6, 0)), - Item::new_positioned(uvec2(6, 2), uvec2(0, 1)), - Item::new_positioned(uvec2(2, 3), uvec2(6, 2)), - Item::new_positioned(uvec2(4, 4), uvec2(0, 4)), - ]; - let door_image = asset_server.load(DOOR_CLOSED_ASSET); - let crate_image = asset_server.load(CRATE_CLOSED_ASSET); - commands.spawn(door_bundle(door_image.clone(), vec2(16., 0.), true)); - commands.spawn(door_bundle(door_image.clone(), vec2(48., 0.), false)); - commands.spawn(door_bundle(door_image.clone(), vec2(80., 0.), false)).insert(Locked); - commands.spawn(wall_bundle(vec2(-48., 0.))); - commands.spawn(crate_bundle(crate_image.clone(), vec2(-32., 0.), uvec2(8, 8), items)); -} diff --git a/src/layout/systems.rs b/src/layout/systems.rs index e69de29..5d42f10 100644 --- a/src/layout/systems.rs +++ b/src/layout/systems.rs @@ -0,0 +1,80 @@ +use bevy::{ + ecs::query::QueryFilter, + prelude::*, +}; +use bevy_rapier2d::prelude::*; + +use crate::{ + inventory::item::Item, + player::Player, +}; + +use super::*; + +fn get_interactive_id( + tested_id: Entity, + interactive_query: Query<(), F>, + parent_query: Query<&ChildOf, With>, +) -> Option { + if interactive_query.get(tested_id).is_ok() { + return Some(tested_id); + } + let Ok(parent_id) = parent_query.get(tested_id) else { + return None; + }; + match interactive_query.get(parent_id.0) { + Ok(_) => Some(parent_id.0), + Err(_) => None, + } +} + +pub fn detect_interact_collisions( + mut commands: Commands, + mut collision_events: MessageReader, + player_query: Query<(), With>, + interactive_query1: Query<(), (With, Without)>, + interactive_query2: Query<(), (With, With)>, + parent_query: Query<&ChildOf, With>, +) { + for collision_event in collision_events.read() { + match collision_event { + CollisionEvent::Started(first, second, _) => { + let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else { + continue; + }; + if player_query.get(*second).is_err() { + continue; + } + commands.entity(interactive_id).insert(MayInteract); + }, + CollisionEvent::Stopped(first, second, _) => { + let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else { + continue; + }; + if player_query.get(*second).is_err() { + continue; + } + commands.entity(interactive_id).remove::(); + }, + } + } +} + +pub fn setup_world( + mut commands: Commands, + asset_server: Res, +) { + let items = vec![ + Item::new_positioned(uvec2(1, 1), uvec2(0, 0)), + Item::new_positioned(uvec2(2, 1), uvec2(1, 0)), + Item::new_positioned(uvec2(3, 1), uvec2(3, 0)), + Item::new_positioned(uvec2(2, 2), uvec2(6, 0)), + Item::new_positioned(uvec2(6, 2), uvec2(0, 1)), + Item::new_positioned(uvec2(2, 3), uvec2(6, 2)), + Item::new_positioned(uvec2(4, 4), uvec2(0, 4)), + ]; + commands.spawn(door::door_bundle(&asset_server, vec2(16., 0.), true)); + commands.spawn(door::door_bundle(&asset_server, vec2(48., 0.), false)); + commands.spawn(door::door_bundle(&asset_server, vec2(80., 0.), false)).insert(Locked); + commands.spawn(container::container_bundle(&asset_server, vec2(-32., 0.), uvec2(8, 8), items)); +} diff --git a/src/lib.rs b/src/lib.rs index cf4a479..6332bca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,12 @@ +use bevy::{ + prelude::*, + ui_widgets::ScrollbarPlugin +}; +use bevy_rapier2d::{ + prelude::*, + rapier::prelude::IntegrationParameters +}; + pub mod player; pub mod layout; pub mod input; @@ -6,28 +15,10 @@ pub mod ui; #[cfg(test)] mod tests; -use bevy::{prelude::*, ui_widgets::ScrollbarPlugin}; -use bevy_rapier2d::{prelude::*, rapier::prelude::IntegrationParameters}; -use leafwing_input_manager::prelude::*; -use serde::{Deserialize, Serialize}; - pub const PIXELS_PER_METER: f32 = 16.0; pub struct ExpeditionPlugin; -#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] -pub enum InputAction { - #[actionlike(Axis)] - Move, - ToggleInventory, - Interact, -} - -#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] -pub enum UiAction { - Rotate, -} - #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] pub enum GameState { #[default] @@ -35,26 +26,6 @@ pub enum GameState { Inventory, } -impl InputAction { - pub fn default_input_map() -> InputMap { - InputMap::default() - .with_axis(Self::Move, VirtualAxis::ad()) - .with_axis(Self::Move, GamepadAxis::LeftStickX) - .with(Self::ToggleInventory, KeyCode::KeyI) - .with(Self::ToggleInventory, GamepadButton::Select) - .with(Self::Interact, KeyCode::KeyE) - .with(Self::Interact, GamepadButton::East) - } -} - -impl UiAction { - pub fn default_input_map() -> InputMap { - InputMap::default() - .with(Self::Rotate, KeyCode::KeyR) - .with(Self::Rotate, GamepadButton::West) - } -} - pub fn insert_entity_name(names: Query<(Entity, &mut Name), Added>) { for (entity, mut name) in names { name.mutate(|name| name.insert_str(0, format!("{entity}: ").as_str())); @@ -87,8 +58,8 @@ fn setup_global(mut commands: Commands) { impl Plugin for ExpeditionPlugin { fn build(&self, app: &mut App) { app.add_plugins(( - input::InputAssetPlugin::::default(), - input::InputAssetPlugin::::default(), + input::plugin::InputAssetPlugin::::default(), + input::plugin::InputAssetPlugin::::default(), ScrollbarPlugin, RapierPhysicsPlugin::<()>::default() .with_custom_initialization(RapierContextInitialization::InitializeDefaultRapierContext { @@ -107,22 +78,16 @@ impl Plugin for ExpeditionPlugin { )) .init_state::() .insert_resource(ui::WindowSize::default()) - .add_systems(Startup, (player::setup_player, setup_global, layout::setup_world)) + .add_systems(Startup, (player::systems::setup_player, setup_global, layout::systems::setup_world)) .add_systems(Update, ( - player::handle_input, + player::systems::handle_input, ui::update_window_size, ui::handle_input, insert_entity_name, - layout::detect_interact_collisions, + layout::systems::detect_interact_collisions, )) - .add_systems(OnEnter(GameState::Inventory), inventory::ui::setup_ui_inventory) - .add_systems(OnExit(GameState::Inventory), inventory::ui::clear_ui_inventory) - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .add_observer(inventory::ui::on_ui_rotate); + .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); } } diff --git a/src/main.rs b/src/main.rs index a162af9..e267a13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,10 @@ -use bevy::{prelude::*, remote::{RemotePlugin, http::RemoteHttpPlugin}}; +use bevy::{ + prelude::*, + remote::{ + RemotePlugin, + http::RemoteHttpPlugin, + }, +}; use expedition_demo::ExpeditionPlugin; diff --git a/src/player/mod.rs b/src/player/mod.rs index 8690b94..3409370 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,8 +1,11 @@ use bevy::prelude::*; use bevy_rapier2d::prelude::*; -use leafwing_input_manager::prelude::*; -use crate::{GameState, InputAction as Action, PIXELS_PER_METER, inventory::{Inventory, item::Item}, layout::{InteractionEvent, MayInteract}}; +use crate::{ + input::InputAction as Action, + PIXELS_PER_METER, + inventory::Inventory, +}; pub mod systems; @@ -12,7 +15,7 @@ pub struct Player { speed: f32, } -fn player_bundle(asset_server: &Res) -> impl Bundle { +pub fn player_bundle(asset_server: &Res) -> impl Bundle { let image = asset_server.load("sprites/player/player.png"); ( Player { @@ -35,83 +38,3 @@ fn player_bundle(asset_server: &Res) -> impl Bundle { ], ) } - -pub fn setup_player(mut commands: Commands, asset_server: Res) { - commands.spawn(player_bundle(&asset_server)); -} - -pub fn try_insert_item( - mut commands: Commands, - item_query: Query<&Item>, - inventory_query: Query<(Entity, &Inventory, Option<&Children>)>, -) { - let mut item = Item::new(UVec2::new(1, 1)); - let name = Name::new(format!("Item {}x{}", item.size.x, item.size.y)); - for (entity, inventory, children) in inventory_query.iter().sort::() { - let children = match children { - Some(children) => &children[..], - None => &[], - }; - match inventory.find_free_space(item_query, children, item.size) { - Some((position, should_rotate)) => { - if should_rotate { - item.rotate(); - } - item.position = Some(position); - info!("Spawning item {item:?}"); - commands.entity(entity).with_child((item, name)); - break; - }, - None => (), - } - } -} - -pub fn handle_input( - mut commands: Commands, - time: Res