Compare commits

...

2 commits

Author SHA1 Message Date
ffdb5d94a8 feat: Derive traits 2026-03-19 15:57:29 +03:00
0a0eb14a6b feat: input plugin reflects, derives 2026-03-19 15:01:04 +03:00
12 changed files with 146 additions and 88 deletions

2
Cargo.lock generated
View file

@ -2422,7 +2422,7 @@ dependencies = [
[[package]] [[package]]
name = "expedition_demo" name = "expedition_demo"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_common_assets", "bevy_common_assets",

View file

@ -1,13 +1,14 @@
use bevy::prelude::*; use bevy::prelude::*;
use leafwing_input_manager::prelude::*; use leafwing_input_manager::prelude::*;
use serde::{ use serde::{
Deserialize,
Serialize, Serialize,
Deserialize
}; };
pub mod plugin; pub mod plugin;
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] #[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(PartialEq, Hash, Debug, Clone, Serialize, Deserialize)]
pub enum InputAction { pub enum InputAction {
#[actionlike(Axis)] #[actionlike(Axis)]
Move, Move,
@ -28,6 +29,7 @@ impl InputAction {
} }
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)] #[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(PartialEq, Hash, Debug, Clone, Serialize, Deserialize)]
pub enum UiAction { pub enum UiAction {
Rotate, Rotate,
} }

View file

@ -1,13 +1,33 @@
use std::{any::{Any, TypeId}, collections::HashMap, hash::Hash, marker::PhantomData}; use std::{
any::{
Any,
TypeId,
},
collections::HashMap,
hash::Hash,
marker::PhantomData,
};
use bevy::{prelude::*, reflect::GetTypeRegistration}; use bevy::{
prelude::*,
reflect::Reflectable,
};
use bevy_common_assets::toml::TomlAssetPlugin; use bevy_common_assets::toml::TomlAssetPlugin;
use leafwing_input_manager::{Actionlike, plugin::InputManagerPlugin, prelude::*}; use leafwing_input_manager::{
use serde::{Deserialize, Serialize, de::DeserializeOwned}; Actionlike,
plugin::InputManagerPlugin,
prelude::*,
};
use serde::{
Deserialize,
Serialize,
de::DeserializeOwned,
};
const INPUT_ASSET_EXTENSIONS: [&'static str; 1] = ["input.toml"]; const INPUT_ASSET_EXTENSIONS: [&'static str; 1] = ["input.toml"];
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Reflect)]
#[reflect(Clone, Debug, Serialize, Deserialize, Default, PartialEq)]
pub struct MultiInput { pub struct MultiInput {
pub keyboard: Option<Vec<KeyCode>>, pub keyboard: Option<Vec<KeyCode>>,
pub mouse: Option<Vec<MouseButton>>, pub mouse: Option<Vec<MouseButton>>,
@ -20,7 +40,8 @@ impl From<MultiInput> for InputKind {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Reflect)]
#[reflect(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum InputKind { pub enum InputKind {
Button(MultiInput), Button(MultiInput),
@ -29,6 +50,7 @@ pub enum InputKind {
} }
#[derive(Default, Deref, DerefMut, Debug, Asset, Reflect, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Default, Deref, DerefMut, Debug, Asset, Reflect, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[reflect(Debug, Clone, PartialEq)]
pub struct InputAsset<Name> pub struct InputAsset<Name>
where Name: Sized + Hash + Eq + Reflect + TypePath + Actionlike { where Name: Sized + Hash + Eq + Reflect + TypePath + Actionlike {
#[serde(flatten)] #[serde(flatten)]
@ -205,17 +227,18 @@ impl<Name> From<InputMap<Name>> for InputAsset<Name>
} }
} }
#[derive(Resource, Deref)] #[derive(Resource, Debug, Deref, DerefMut, Reflect, Clone, PartialEq, Eq)]
pub struct InputAssetHandle<T: Sized + Hash + Eq + Reflect + TypePath + Actionlike> (Option<Handle<InputAsset<T>>>); #[reflect(Resource, Debug, Clone, PartialEq)]
pub struct InputAssetHandle<T: Sized + Hash + Eq + Reflectable + Actionlike> (Option<Handle<InputAsset<T>>>);
#[derive(Debug)] #[derive(Debug)]
pub struct InputAssetPlugin<T: Sized + Hash + Eq + Reflect + TypePath + Actionlike + DeserializeOwned + GetTypeRegistration> { pub struct InputAssetPlugin<T: Sized + Hash + Eq + Reflectable + Actionlike + DeserializeOwned> {
_phantom: PhantomData<T>, _phantom: PhantomData<T>,
extensions: &'static [&'static str], extensions: &'static [&'static str],
} }
impl<T> InputAssetPlugin<T> impl<T> InputAssetPlugin<T>
where T: Sized + Hash + Eq + Reflect + TypePath + Actionlike + DeserializeOwned + GetTypeRegistration { where T: Sized + Hash + Eq + Reflectable + Actionlike + DeserializeOwned {
pub fn new(extensions: &'static [&'static str]) -> Self { pub fn new(extensions: &'static [&'static str]) -> Self {
Self { Self {
_phantom: PhantomData, _phantom: PhantomData,
@ -225,7 +248,7 @@ impl<T> InputAssetPlugin<T>
} }
impl<T> Default for InputAssetPlugin<T> impl<T> Default for InputAssetPlugin<T>
where T: Sized + Hash + Eq + Reflect + TypePath + Actionlike + DeserializeOwned + GetTypeRegistration { where T: Sized + Hash + Eq + Reflectable + Actionlike + DeserializeOwned {
fn default() -> Self { fn default() -> Self {
Self { Self {
_phantom: PhantomData, _phantom: PhantomData,
@ -235,12 +258,11 @@ impl<T> Default for InputAssetPlugin<T>
} }
impl<T> Plugin for InputAssetPlugin<T> impl<T> Plugin for InputAssetPlugin<T>
where T: Sized + Hash + Eq + Reflect + TypePath + Actionlike + DeserializeOwned + GetTypeRegistration where T: Sized + Hash + Eq + Reflectable + Actionlike + DeserializeOwned {
{
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(( app.add_plugins((
InputManagerPlugin::<T>::default(),
TomlAssetPlugin::<InputAsset<T>>::new(&self.extensions), TomlAssetPlugin::<InputAsset<T>>::new(&self.extensions),
InputManagerPlugin::<T>::default()
)); ));
} }
} }

View file

@ -2,7 +2,8 @@ use std::mem::swap;
use bevy::prelude::*; use bevy::prelude::*;
#[derive(Component, Clone, Debug, Reflect)] #[derive(Component, Clone, Debug, Reflect, PartialEq, Eq)]
#[reflect(Component, Clone, Debug, PartialEq)]
pub struct Item { pub struct Item {
pub size: UVec2, pub size: UVec2,
pub position: Option<UVec2>, pub position: Option<UVec2>,

View file

@ -2,15 +2,17 @@ use bevy::prelude::*;
pub mod item; pub mod item;
#[derive(Component, Reflect, Default)] /// Marker that this inventory will show up when UI is built
#[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq, Eq)]
#[reflect(Component, Clone, Default, Debug, PartialEq)]
pub struct ActiveInventory;
#[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq, Eq)]
#[reflect(Component, Clone, Default, Debug, PartialEq)]
pub struct Inventory { pub struct Inventory {
pub size: UVec2, pub size: UVec2,
} }
/// Marker that this inventory will show up when UI is built
#[derive(Component, Reflect)]
pub struct ActiveInventory;
impl Inventory { impl Inventory {
pub fn new(size: UVec2) -> Self { pub fn new(size: UVec2) -> Self {
Self { size } Self { size }
@ -58,6 +60,23 @@ impl Inventory {
self.can_fit(item_query, children.as_slice(), *size, queried_position) self.can_fit(item_query, children.as_slice(), *size, queried_position)
} }
pub fn can_replace(
&self,
item_query: Query<&item::Item>,
contained_items: &[Entity],
replacable_item: Entity,
queried_item: &item::Item,
) -> bool {
let Some(position) = &queried_item.position else {
warn!("Trying to query rotated item without position");
return false;
};
let children = contained_items.iter()
.filter_map(|e| if e.ne(&&replacable_item) { Some(*e) } else { None })
.collect::<Vec<Entity>>();
self.can_fit(item_query, children.as_slice(), queried_item.size, *position)
}
pub fn can_rotate( pub fn can_rotate(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&item::Item>,
@ -79,23 +98,6 @@ impl Inventory {
self.can_fit(item_query, children.as_slice(), rotated_item.size, *position) self.can_fit(item_query, children.as_slice(), rotated_item.size, *position)
} }
pub fn can_replace(
&self,
item_query: Query<&item::Item>,
contained_items: &[Entity],
replacable_item: Entity,
queried_item: &item::Item,
) -> bool {
let Some(position) = &queried_item.position else {
warn!("Trying to query rotated item without position");
return false;
};
let children = contained_items.iter()
.filter_map(|e| if e.ne(&&replacable_item) { Some(*e) } else { None })
.collect::<Vec<Entity>>();
self.can_fit(item_query, children.as_slice(), queried_item.size, *position)
}
fn find_free_space_inner( fn find_free_space_inner(
&self, &self,
item_query: Query<&item::Item>, item_query: Query<&item::Item>,

View file

@ -11,11 +11,12 @@ use crate::{
}, },
}; };
const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png";
use super::*; use super::*;
#[derive(Component)] const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png";
#[derive(Component, Clone, Copy, Default, Reflect, Debug, PartialEq, Eq)]
#[reflect(Component, Clone, Default, Debug, PartialEq)]
#[require(Sprite, InteractiveObject, Inventory)] #[require(Sprite, InteractiveObject, Inventory)]
pub struct Container; pub struct Container;

View file

@ -8,10 +8,17 @@ use super::*;
const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png"; const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png";
const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png"; const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png";
#[derive(Component)] #[derive(Component, Clone, Copy, Reflect, PartialEq, Eq, Debug)]
#[reflect(Component, Clone, Default, PartialEq, Debug)]
#[require(Sprite, InteractiveObject)] #[require(Sprite, InteractiveObject)]
pub struct Door(pub i8); pub struct Door(pub i8);
impl Default for Door {
fn default() -> Self {
Self(1)
}
}
fn on_door_interact( fn on_door_interact(
event: On<InteractionEvent>, event: On<InteractionEvent>,
mut commands: Commands, mut commands: Commands,

View file

@ -4,16 +4,20 @@ pub mod container;
pub mod door; pub mod door;
pub mod systems; pub mod systems;
#[derive(Component)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct MayInteract; pub struct MayInteract;
#[derive(Component, Default)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct InteractiveObject; pub struct InteractiveObject;
#[derive(Component)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct Locked; pub struct Locked;
#[derive(EntityEvent)] #[derive(EntityEvent, Reflect, Clone, Copy, PartialEq, Eq, Debug)]
#[reflect(Event, Debug, PartialEq, Clone)]
pub struct InteractionEvent { pub struct InteractionEvent {
pub entity: Entity, pub entity: Entity,
} }

View file

@ -1,37 +1,32 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
ui_widgets::ScrollbarPlugin ui_widgets::ScrollbarPlugin,
}; };
use bevy_rapier2d::{ use bevy_rapier2d::{
prelude::*, prelude::*,
rapier::prelude::IntegrationParameters rapier::prelude::IntegrationParameters,
}; };
pub mod player;
pub mod layout;
pub mod input; pub mod input;
pub mod inventory; pub mod inventory;
pub mod ui; pub mod layout;
pub mod player;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod ui;
pub const PIXELS_PER_METER: f32 = 16.0; pub const PIXELS_PER_METER: f32 = 16.0;
pub struct ExpeditionPlugin; pub struct ExpeditionPlugin;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States, Reflect)]
#[reflect(Clone, PartialEq, Debug, Default, Hash, State)]
pub enum GameState { pub enum GameState {
#[default] #[default]
Running, Running,
Inventory, Inventory,
} }
pub fn insert_entity_name(names: Query<(Entity, &mut Name), Added<Name>>) {
for (entity, mut name) in names {
name.mutate(|name| name.insert_str(0, format!("{entity}: ").as_str()));
}
}
fn camera_bundle() -> impl Bundle { fn camera_bundle() -> impl Bundle {
( (
Camera2d, Camera2d,
@ -50,6 +45,12 @@ fn camera_bundle() -> impl Bundle {
) )
} }
fn insert_entity_name(names: Query<(Entity, &mut Name), Added<Name>>) {
for (entity, mut name) in names {
name.mutate(|name| name.insert_str(0, format!("{entity}: ").as_str()));
}
}
fn setup_global(mut commands: Commands) { fn setup_global(mut commands: Commands) {
commands.spawn(camera_bundle()); commands.spawn(camera_bundle());
commands.spawn(ui::UiRoot::new()); commands.spawn(ui::UiRoot::new());
@ -57,12 +58,7 @@ fn setup_global(mut commands: Commands) {
impl Plugin for ExpeditionPlugin { impl Plugin for ExpeditionPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(( let rapier_init = RapierContextInitialization::InitializeDefaultRapierContext {
input::plugin::InputAssetPlugin::<input::InputAction>::default(),
input::plugin::InputAssetPlugin::<input::UiAction>::default(),
ScrollbarPlugin,
RapierPhysicsPlugin::<()>::default()
.with_custom_initialization(RapierContextInitialization::InitializeDefaultRapierContext {
integration_parameters: IntegrationParameters { integration_parameters: IntegrationParameters {
length_unit: PIXELS_PER_METER, length_unit: PIXELS_PER_METER,
..default() ..default()
@ -73,18 +69,28 @@ impl Plugin for ExpeditionPlugin {
scaled_shape_subdivision: 10, scaled_shape_subdivision: 10,
force_update_from_transform_changes: false, force_update_from_transform_changes: false,
}, },
}), };
app.add_plugins((
RapierDebugRenderPlugin::default(), RapierDebugRenderPlugin::default(),
RapierPhysicsPlugin::<()>::default()
.with_custom_initialization(rapier_init),
ScrollbarPlugin,
input::plugin::InputAssetPlugin::<input::InputAction>::default(),
input::plugin::InputAssetPlugin::<input::UiAction>::default(),
)) ))
.init_state::<GameState>() .init_state::<GameState>()
.insert_resource(ui::WindowSize::default()) .insert_resource(ui::WindowSize::default())
.add_systems(Startup, (player::systems::setup_player, setup_global, layout::systems::setup_world)) .add_systems(Startup, (
setup_global,
layout::systems::setup_world,
player::systems::setup_player,
))
.add_systems(Update, ( .add_systems(Update, (
insert_entity_name,
layout::systems::detect_interact_collisions,
player::systems::handle_input, player::systems::handle_input,
ui::update_window_size, ui::update_window_size,
ui::handle_input, ui::handle_input,
insert_entity_name,
layout::systems::detect_interact_collisions,
)) ))
.add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory) .add_systems(OnEnter(GameState::Inventory), ui::inventory::systems::setup_ui_inventory)
.add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory) .add_systems(OnExit(GameState::Inventory), ui::inventory::systems::clear_ui_inventory)

View file

@ -9,18 +9,23 @@ use crate::{
pub mod systems; pub mod systems;
#[derive(Component, Reflect)] #[derive(Component, Clone, Copy, Reflect, PartialEq, Debug)]
#[reflect(Component, Clone, Default, PartialEq, Debug)]
pub struct Player { pub struct Player {
// px/s // px/s
speed: f32, speed: f32,
} }
impl Default for Player {
fn default() -> Self {
Self { speed: PIXELS_PER_METER * 0.8 }
}
}
pub fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle { pub fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
let image = asset_server.load("sprites/player/player.png"); let image = asset_server.load("sprites/player/player.png");
( (
Player { Player::default(),
speed: PIXELS_PER_METER * 0.8,
},
Sprite::from_image(image), Sprite::from_image(image),
Transform::from_xyz(0f32, 0f32, 1f32), Transform::from_xyz(0f32, 0f32, 1f32),
Action::default_input_map(), Action::default_input_map(),

View file

@ -14,29 +14,36 @@ pub mod bundles;
pub mod observers; pub mod observers;
pub mod systems; pub mod systems;
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
#[require(Node)] #[require(Node)]
pub struct UiInventoryManager; pub struct UiInventoryManager;
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node)] #[require(Node)]
pub struct UiInventory(pub Entity); pub struct UiInventory(pub Entity);
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
#[require(Node, ImageNode)] #[require(Node, ImageNode)]
pub struct UiInventorySlot(pub UVec2); pub struct UiInventorySlot(pub UVec2);
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node, ImageNode)] #[require(Node, ImageNode)]
pub struct UiItem(pub Entity); pub struct UiItem(pub Entity);
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredItem; pub struct HoveredItem;
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredSlot; pub struct HoveredSlot;
#[derive(Component, Reflect)] #[derive(Component, Debug, PartialEq, Eq, Clone, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct DraggedItem(pub Item, pub UVec2); pub struct DraggedItem(pub Item, pub UVec2);
fn ui_item_node_data(item: &Item) -> (Val, Val, Val, Val, UiTransform) { fn ui_item_node_data(item: &Item) -> (Val, Val, Val, Val, UiTransform) {

View file

@ -8,8 +8,9 @@ use crate::input::UiAction;
pub mod inventory; pub mod inventory;
#[derive(Component, Reflect)] #[derive(Component, Reflect, Debug, Default, PartialEq, Eq, Clone, Copy)]
#[require(Node)] #[require(Node)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
pub struct UiRoot; pub struct UiRoot;
#[derive(Resource, Deref, DerefMut, Default)] #[derive(Resource, Deref, DerefMut, Default)]