From be36504a00254d84953ad4a405dbbe7fae70ea83 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 21 Apr 2026 13:09:03 +0300 Subject: [PATCH 1/2] feat: More AnimatedSprite methods --- src/anim/sprite.rs | 62 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/src/anim/sprite.rs b/src/anim/sprite.rs index 385ae7f..14053fc 100644 --- a/src/anim/sprite.rs +++ b/src/anim/sprite.rs @@ -5,6 +5,8 @@ use std::{collections::HashMap, time::Duration}; use bevy::prelude::*; use bevy_ecs::system::EntityEntryCommands; +type SpriteCommands<'a> = EntityEntryCommands<'a, Sprite>; + /// Data for [AnimatedSprite] #[derive(Clone, PartialEq, Debug, Default, Reflect)] #[reflect(Clone, PartialEq, Debug, Default)] @@ -19,6 +21,7 @@ pub struct AnimationData { impl AnimationData { /// Construct new [AnimationData] from given [Handle] + #[inline(always)] pub fn new(image: Handle, frames: Handle, duration_multiplier: f32) -> Self { Self { image, frames, duration_multiplier } } @@ -33,36 +36,41 @@ pub struct AnimatedSprite { current: Option, timer: Timer, duration: Duration, + backwards: bool, } impl AnimatedSprite { /// Constructs new AnimatedSprite + #[inline(always)] pub fn new(data: HashMap, duration: Duration) -> Self { let timer = Timer::new(duration, TimerMode::Once); - Self { data, current: None, timer, duration } + Self { data, current: None, timer, duration, backwards: false } } /// Add new animation to sprite + #[inline(always)] pub fn add(&mut self, data: AnimationData, label: String) { self.data.insert(label, data); } /// Sets internal duration + #[inline(always)] pub fn set_duration(&mut self, duration: Duration) { self.duration = duration; } - /// Finds animation by its label and returns it if started playing - pub fn play( + fn play_inner( &mut self, label: &str, mode: TimerMode, - mut sprite: EntityEntryCommands, + mut sprite: SpriteCommands, + backwards: bool, ) -> Option<&AnimationData> { self.data.get(label).map(|data| { self.current = Some(data.clone()); self.timer = Timer::new(self.duration.mul_f32(data.duration_multiplier), mode); + self.backwards = backwards; let sprite_image = data.image.clone(); let layout = data.frames.clone(); @@ -75,15 +83,55 @@ impl AnimatedSprite { data }) } + + /// Finds animation by its label, plays it and returns it if started playing + #[inline(always)] + pub fn play( + &mut self, + label: &str, + mode: TimerMode, + sprite: SpriteCommands, + ) -> Option<&AnimationData> { + self.play_inner(label, mode, sprite, false) + } + + /// Same as [play](AnimatedSprite::play), but animation plays from the end to the beginning + #[inline(always)] + pub fn play_backwards( + &mut self, + label: &str, + mode: TimerMode, + sprite: SpriteCommands, + ) -> Option<&AnimationData> { + self.play_inner(label, mode, sprite, true) + } + + /// Stops the animation + #[inline(always)] + pub fn stop(&mut self) { + self.current = None; + self.timer.reset(); + } /// Updates animation timer + #[inline(always)] pub fn tick(&mut self, dt: Duration) { self.timer.tick(dt); } - /// Returns inner [Timer::fraction] + /// Returns inner [Timer::fraction] or [Timer::fraction_remaining] if playing backwards + #[inline(always)] pub fn fraction(&self) -> f32 { - self.timer.fraction() + match self.backwards { + true => self.timer.fraction_remaining(), + false => self.timer.fraction(), + } + } + + /// Returns [Timer::mode] + #[inline(always)] + pub fn mode(&self) -> TimerMode { + self.timer.mode() } } @@ -107,7 +155,7 @@ pub fn update_animated_sprites( let max_frames = atlas.len() as f32; - let current_frame = (max_frames * animation.fraction()) as usize; + let current_frame = (max_frames * animation.fraction()).min(max_frames - 1.) as usize; if let Some(atlas) = &mut sprite.texture_atlas { atlas.index = current_frame; From a41cf4879e94e0b8960457df695cb1f662373f63 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 21 Apr 2026 17:08:33 +0300 Subject: [PATCH 2/2] feat: Dummy enemy and attack hit detection --- src/ai/dummy.rs | 32 +++++++++++++++++++++++++++ src/ai/mod.rs | 6 ++++++ src/combat/attack.rs | 51 ++++++++++++++++++++++++++++++++++++++++---- src/combat/mod.rs | 46 +++++++++++++++++++++++++++++++++++++-- src/lib.rs | 14 ++++++------ src/player/mod.rs | 3 ++- src/plugin.rs | 2 +- 7 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 src/ai/dummy.rs create mode 100644 src/ai/mod.rs diff --git a/src/ai/dummy.rs b/src/ai/dummy.rs new file mode 100644 index 0000000..dd172a5 --- /dev/null +++ b/src/ai/dummy.rs @@ -0,0 +1,32 @@ +//! Dummy enemy module that does nothing + +use super::*; + +use crate::{GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, combat::Health, meters}; + +/// Dummy enemy. Has [Health] component and no AI logic +#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)] +#[reflect(Component, Clone, PartialEq, Debug, Default)] +#[require(Health)] +pub struct Dummy; + +impl Dummy { + /// Constructs [Dummy] bundle without sprite + pub fn bundle(position: Vec2, health: f32) -> impl Bundle { + ( + // Basic + Dummy, + Health::new(health), + + // World + Transform::from_xyz(position.x, position.y, 0.), + + // Collision + Collider::cuboid(meters(0.5), meters(0.5)), + CollisionGroups::new( + GROUP_ENEMY, + GROUP_STATIC | GROUP_INTERACTIVE | GROUP_FRIENDLY | GROUP_ATTACK, + ), + ) + } +} diff --git a/src/ai/mod.rs b/src/ai/mod.rs new file mode 100644 index 0000000..535002d --- /dev/null +++ b/src/ai/mod.rs @@ -0,0 +1,6 @@ +//! Enemy AI module + +use bevy::prelude::*; +use bevy_rapier2d::prelude::*; + +pub mod dummy; diff --git a/src/combat/attack.rs b/src/combat/attack.rs index 95ead23..9146905 100644 --- a/src/combat/attack.rs +++ b/src/combat/attack.rs @@ -5,7 +5,7 @@ use std::time::Duration; use bevy::prelude::*; use bevy_rapier2d::prelude::*; -use crate::{GROUP_ATTACK, GROUP_INTERACTIVE, GROUP_STATIC}; +use crate::{GROUP_ATTACK, GROUP_INTERACTIVE, GROUP_STATIC, combat::Health}; /// Contains logic for attack-related calculations #[derive(Component, Clone, Debug, Reflect)] @@ -27,6 +27,7 @@ pub struct AttackAreaOrigin; impl AttackArea { /// Constructs new attack area + #[inline(always)] pub fn new(accuracy: f32, damage: f32, duration: Duration, half_area: Vec2) -> Self { let max_distance = ((half_area.x * 2.).powi(2) + half_area.y.powi(2)).sqrt(); Self { @@ -58,6 +59,9 @@ impl AttackArea { GROUP_STATIC | GROUP_INTERACTIVE | affinity, ), Collider::cuboid(half_area.x, half_area.y), + ActiveCollisionTypes::all(), + Sleeping::disabled(), + ActiveEvents::COLLISION_EVENTS, // AttackAreaOrigin Children::spawn(( @@ -70,11 +74,19 @@ impl AttackArea { } /// Updates timer and returns true if attack has ended + #[inline(always)] pub fn tick(&mut self, dt: Duration) -> bool { self.timer.tick(dt).is_finished() } + /// Calls [Timer::almost_finish] + #[inline(always)] + pub fn almost_finish(&mut self) { + self.timer.almost_finish(); + } + /// Returns damage for given distance to origin + #[inline(always)] pub fn damage(&self, distance_to_origin: f32) -> f32 { let distance_ratio = 1. - distance_to_origin.min(self.max_distance) / self.max_distance; let total_multiplier = self.accuracy * distance_ratio; @@ -82,11 +94,42 @@ impl AttackArea { } } -/// System that updates AttackArea timers and despawns those who timed out -pub fn update_attack_areas(mut commands: Commands, time: Res