feat: Collision groups and states

This commit is contained in:
Alexey 2026-04-13 17:08:25 +03:00
commit 88a73275ff
8 changed files with 108 additions and 3 deletions

18
Cargo.lock generated
View file

@ -591,6 +591,7 @@ dependencies = [
"bevy_rapier2d", "bevy_rapier2d",
"leafwing-input-manager", "leafwing-input-manager",
"petgraph", "petgraph",
"seldom_state",
] ]
[[package]] [[package]]
@ -4816,6 +4817,23 @@ dependencies = [
"tiny-skia", "tiny-skia",
] ]
[[package]]
name = "seldom_state"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db0767975eeb39d99493b23adc55b14d7106c333c06dd85774fa2cb909b79d5"
dependencies = [
"bevy_app",
"bevy_derive",
"bevy_ecs",
"bevy_log",
"bevy_math",
"bevy_utils",
"either",
"leafwing-input-manager",
"variadics_please",
]
[[package]] [[package]]
name = "self_cell" name = "self_cell"
version = "1.2.2" version = "1.2.2"

View file

@ -10,6 +10,7 @@ bevy = { version = "0.18.0", features = ["bevy_remote"] }
bevy_rapier2d = "0.33.0" bevy_rapier2d = "0.33.0"
leafwing-input-manager = "0.20.0" leafwing-input-manager = "0.20.0"
petgraph = { version = "0.8.3" } petgraph = { version = "0.8.3" }
seldom_state = { version = "0.16.0", features = ["leafwing_input"] }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

View file

@ -5,6 +5,8 @@ use std::time::Duration;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use crate::{GROUP_ATTACK, GROUP_INTERACTIVE, GROUP_STATIC};
/// Contains logic for attack-related calculations /// Contains logic for attack-related calculations
#[derive(Component, Clone, Debug, Reflect)] #[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Clone, Debug)] #[reflect(Component, Clone, Debug)]
@ -33,6 +35,7 @@ impl AttackArea {
max_distance, max_distance,
} }
} }
/// Returns attack area bundle with everything needed /// Returns attack area bundle with everything needed
pub fn bundle( pub fn bundle(
damage: f32, damage: f32,
@ -41,6 +44,7 @@ impl AttackArea {
position: Vec2, position: Vec2,
half_area: Vec2, half_area: Vec2,
facing_left: bool, facing_left: bool,
affinity: Group,
) -> impl Bundle { ) -> impl Bundle {
let origin_x = if facing_left { position.x - half_area.x } else { position.x + half_area.x }; let origin_x = if facing_left { position.x - half_area.x } else { position.x + half_area.x };
( (
@ -49,6 +53,10 @@ impl AttackArea {
// Collision // Collision
Transform::from_xyz(position.x, position.y, 0.), Transform::from_xyz(position.x, position.y, 0.),
CollisionGroups::new(
GROUP_ATTACK,
GROUP_STATIC | GROUP_INTERACTIVE | affinity,
),
Collider::cuboid(half_area.x, half_area.y), Collider::cuboid(half_area.x, half_area.y),
// AttackAreaOrigin // AttackAreaOrigin
@ -73,3 +81,12 @@ impl AttackArea {
self.damage * EaseFunction::QuinticOut.sample_unchecked(total_multiplier) self.damage * EaseFunction::QuinticOut.sample_unchecked(total_multiplier)
} }
} }
/// System that updates AttackArea timers and despawns those who timed out
pub fn update_attack_areas(mut commands: Commands, time: Res<Time>, areas: Query<(Entity, &mut AttackArea)>) {
for (area_id, mut area) in areas {
if area.tick(time.delta()) {
commands.entity(area_id).despawn();
}
}
}

View file

@ -1,3 +1,25 @@
//! This module contains core concepts of the combat system //! This module contains core concepts of the combat system
use bevy::prelude::*;
pub mod attack; pub mod attack;
/// Combat health component
#[derive(Component, Clone, Copy, PartialEq, Debug, Default, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug, Default)]
pub struct Health {
/// Current health value
pub current: f32,
/// Maximum health value
pub max: f32,
}
impl Health {
/// Constructs new health component
pub fn new(max: f32) -> Self {
Self {
current: max,
max,
}
}
}

View file

@ -10,14 +10,27 @@ pub mod graph;
pub mod input; pub mod input;
pub mod player; pub mod player;
pub mod plugin; pub mod plugin;
pub mod states;
pub mod timer; pub mod timer;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use crate::{player::Player, timer::BpmTimer}; use crate::{player::Player, timer::BpmTimer};
const PIXELS_PER_METER: f32 = 16.; const PIXELS_PER_METER: f32 = 16.;
/// Collision group for static entities (walls, floors, etc)
pub const GROUP_STATIC: Group = Group::GROUP_1;
/// Collision group for interactive entities (doors, buttons, etc)
pub const GROUP_INTERACTIVE: Group = Group::GROUP_2;
/// Collision group for player and player-related entities
pub const GROUP_FRIENDLY: Group = Group::GROUP_3;
/// Collision group for enemy-related entities
pub const GROUP_ENEMY: Group = Group::GROUP_4;
/// Collision group for attack-related entities
pub const GROUP_ATTACK: Group = Group::GROUP_5;
/// Returns pixel measurement for given length in meters /// Returns pixel measurement for given length in meters
#[inline(always)] pub const fn meters(length: f32) -> f32 { PIXELS_PER_METER * length } #[inline(always)] pub const fn meters(length: f32) -> f32 { PIXELS_PER_METER * length }

View file

@ -5,11 +5,10 @@ use bevy_rapier2d::prelude::*;
use leafwing_input_manager::prelude::*; use leafwing_input_manager::prelude::*;
use crate::{ use crate::{
meters, GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, input::{
input::{
DefaultInputMap, DefaultInputMap,
PlayerInput, PlayerInput,
} }, meters
}; };
pub mod systems; pub mod systems;
@ -51,11 +50,19 @@ impl Player {
RigidBody::KinematicPositionBased, RigidBody::KinematicPositionBased,
KinematicCharacterController::default(), KinematicCharacterController::default(),
ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC, ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC,
CollisionGroups::new(
GROUP_FRIENDLY,
GROUP_STATIC | GROUP_INTERACTIVE | GROUP_ENEMY | GROUP_ATTACK,
),
Collider::cuboid(meters(0.3), meters(0.9)), Collider::cuboid(meters(0.3), meters(0.9)),
ActiveEvents::COLLISION_EVENTS, ActiveEvents::COLLISION_EVENTS,
Sleeping::disabled(), Sleeping::disabled(),
// State
crate::states::Free,
Children::spawn(( Children::spawn((
// Camera
Spawn(( Spawn((
Name::new("Player camera"), Name::new("Player camera"),
Camera2d, Camera2d,

View file

@ -3,6 +3,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use leafwing_input_manager::prelude::*; use leafwing_input_manager::prelude::*;
use seldom_state::prelude::*;
use crate::*; use crate::*;
@ -18,6 +19,8 @@ impl Plugin for GamePlugin {
InputManagerPlugin::<input::PlayerInput>::default(), InputManagerPlugin::<input::PlayerInput>::default(),
InputManagerPlugin::<input::DebugInput>::default(), InputManagerPlugin::<input::DebugInput>::default(),
StateMachinePlugin::default(),
)) ))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, ( .add_systems(Update, (

24
src/states.rs Normal file
View file

@ -0,0 +1,24 @@
//! Commonly used entity states
use bevy::prelude::*;
/// Entity is not doing anything special
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug, Default)]
pub struct Free;
/// Entity is acting and cannot do something else
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug, Default)]
pub struct Acting {
/// How much time left before action ends
pub beats_remaining: u32,
}
/// Entity is awaiting a specific set of actions
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug, Default)]
pub struct ActionWindow {
/// How much time left before window ends
pub beats_remaining: u32,
}