diff --git a/src/combat/attack.rs b/src/combat/attack.rs new file mode 100644 index 0000000..eceaf54 --- /dev/null +++ b/src/combat/attack.rs @@ -0,0 +1,75 @@ +//! Attack module + +use std::time::Duration; + +use bevy::prelude::*; +use bevy_rapier2d::prelude::*; + +/// Contains logic for attack-related calculations +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone, Debug)] +#[require(Transform, Collider, Sensor)] +pub struct AttackArea { + accuracy: f32, + damage: f32, + timer: Timer, + max_distance: f32, +} + +/// Determines [AttackArea] damage based on distance of target from itself +#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect, Default)] +#[reflect(Component, Clone, PartialEq, Debug, Default)] +#[require(Transform)] +pub struct AttackAreaOrigin; + +impl AttackArea { + /// Constructs new attack area + 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 { + accuracy, + damage, + timer: Timer::new(duration, TimerMode::Once), + max_distance, + } + } + /// Returns attack area bundle with everything needed + pub fn bundle( + damage: f32, + accuracy: f32, + duration: Duration, + position: Vec2, + half_area: Vec2, + facing_left: bool, + ) -> impl Bundle { + let origin_x = if facing_left { position.x - half_area.x } else { position.x + half_area.x }; + ( + // Basic + AttackArea::new(accuracy, damage, duration, half_area), + + // Collision + Transform::from_xyz(position.x, position.y, 0.), + Collider::cuboid(half_area.x, half_area.y), + + // AttackAreaOrigin + Children::spawn(( + Spawn(( + AttackAreaOrigin, + Transform::from_xyz(origin_x, 0., 0.), + )), + )), + ) + } + + /// Updates timer and returns true if attack has ended + pub fn tick(&mut self, dt: Duration) -> bool { + self.timer.tick(dt).is_finished() + } + + /// Returns damage for given distance to origin + 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; + self.damage * EaseFunction::QuinticOut.sample_unchecked(total_multiplier) + } +} diff --git a/src/combat/mod.rs b/src/combat/mod.rs new file mode 100644 index 0000000..a8e6a95 --- /dev/null +++ b/src/combat/mod.rs @@ -0,0 +1,3 @@ +//! This module contains core concepts of the combat system + +pub mod attack; diff --git a/src/lib.rs b/src/lib.rs index 94717b2..bc03532 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,14 +5,16 @@ #[cfg(test)] mod tests; +pub mod combat; pub mod graph; pub mod input; pub mod player; pub mod plugin; +pub mod timer; use bevy::prelude::*; -use crate::player::Player; +use crate::{player::Player, timer::BpmTimer}; const PIXELS_PER_METER: f32 = 16.; @@ -23,4 +25,5 @@ const PIXELS_PER_METER: f32 = 16.; /// Temporary function to setup world pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Player::bundle(&asset_server, Vec2::ZERO)); + commands.spawn(BpmTimer::new(120.)); } diff --git a/src/plugin.rs b/src/plugin.rs index b693cc4..a7efe34 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -20,6 +20,9 @@ impl Plugin for GamePlugin { InputManagerPlugin::::default(), )) .add_systems(Startup, setup) - .add_systems(Update, player::systems::handle_input); + .add_systems(Update, ( + player::systems::handle_input, + timer::update_bpm_timers, + )); } } diff --git a/src/tests/attack.rs b/src/tests/attack.rs new file mode 100644 index 0000000..afea40e --- /dev/null +++ b/src/tests/attack.rs @@ -0,0 +1,26 @@ +use std::time::Duration; + +use bevy::prelude::*; + +use crate::combat::attack::AttackArea; + +#[test] +fn attack_deals_max_damage() { + let a = AttackArea::new(1., 1., Duration::from_secs(1), vec2(1., 1.)); + + assert_eq!(a.damage(0.), 1.); +} + +#[test] +fn attack_too_far() { + let a = AttackArea::new(1., 1., Duration::from_secs(1), vec2(1., 1.)); + + assert_eq!(a.damage(5f32.sqrt()), 0.); +} + +#[test] +fn attack_too_inaccurate() { + let a = AttackArea::new(0., 1., Duration::from_secs(1), vec2(1., 1.)); + + assert_eq!(a.damage(0.), 0.); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 88f0f58..071785b 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1 +1,2 @@ +mod attack; mod graph; diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..c8b29b4 --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,85 @@ +//! Beats per minute based timer module + +use std::time::Duration; + +use bevy::prelude::*; + +/// Calculates [Duration] from BPM +#[inline(always)] +pub fn duration_from_bpm(bpm: f32) -> Duration { + Duration::from_millis((60_000. / bpm) as u64) +} + +/// Calculates BPM from [Duration] +#[inline(always)] +pub fn bpm_from_duration(dur: Duration) -> f32 { + 60_000. / (dur.as_millis() as f32) +} + +/// Event that is triggered when [BpmTimer] just ticked +#[derive(EntityEvent, Clone, Copy, PartialEq, Debug, Reflect)] +#[reflect(Event, Clone, PartialEq, Debug)] +pub struct TickEvent { + /// [BpmTimer] that triggered event + #[event_target] pub timer: Entity, + /// Total elapsed time of that timer in beats + pub elapsed: f32, +} + +/// Timer that is based on beats per minute (BPM) value +/// Ticks every beat, counts total ticks and triggers [TickEvent] +#[derive(Component, Clone, PartialEq, Debug, Reflect)] +#[reflect(Component, Clone, PartialEq, Debug)] +pub struct BpmTimer { + timer: Timer, + elapsed: f32, +} + +impl BpmTimer { + /// Constructs new BpmTimer from given BPM + pub fn new(bpm: f32) -> Self { + Self { + timer: Timer::new(duration_from_bpm(bpm), TimerMode::Repeating), + elapsed: 0., + } + } + + /// Updates internal timer and returns true if it just ticked + pub fn tick(&mut self, dt: Duration) -> bool { + let ticked = self.timer.tick(dt).just_finished(); + if ticked { self.elapsed += 1.; } + ticked + } + + /// Returns total elapsed time in beats + #[inline(always)] + pub fn beats_elapsed(&self) -> f32 { + self.elapsed + self.timer.fraction() + } + + /// Get beats per minute value of timer + #[inline(always)] + pub fn get_bpm(&self) -> f32 { + bpm_from_duration(self.timer.duration()) + } + + /// Set new BPM value + pub fn set_bpm(&mut self, new_bpm: f32) { + self.timer.set_duration(duration_from_bpm(new_bpm)); + } + + /// Resets elapsed beats + pub fn reset(&mut self) { + self.timer.reset(); + self.elapsed = 0.; + } +} + +/// System that ticks each [BpmTimer] and triggers [TickEvent]s +pub fn update_bpm_timers(mut commands: Commands, time: Res