generated from 2ndbeam/bevy-template
feat: BpmTimer and AttackArea
This commit is contained in:
parent
e4b1475c48
commit
f6022d84b2
7 changed files with 198 additions and 2 deletions
75
src/combat/attack.rs
Normal file
75
src/combat/attack.rs
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/combat/mod.rs
Normal file
3
src/combat/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
//! This module contains core concepts of the combat system
|
||||||
|
|
||||||
|
pub mod attack;
|
||||||
|
|
@ -5,14 +5,16 @@
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub mod combat;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
|
pub mod timer;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::player::Player;
|
use crate::{player::Player, timer::BpmTimer};
|
||||||
|
|
||||||
const PIXELS_PER_METER: f32 = 16.;
|
const PIXELS_PER_METER: f32 = 16.;
|
||||||
|
|
||||||
|
|
@ -23,4 +25,5 @@ const PIXELS_PER_METER: f32 = 16.;
|
||||||
/// Temporary function to setup world
|
/// Temporary function to setup world
|
||||||
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
|
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
|
||||||
|
commands.spawn(BpmTimer::new(120.));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ impl Plugin for GamePlugin {
|
||||||
InputManagerPlugin::<input::DebugInput>::default(),
|
InputManagerPlugin::<input::DebugInput>::default(),
|
||||||
))
|
))
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, player::systems::handle_input);
|
.add_systems(Update, (
|
||||||
|
player::systems::handle_input,
|
||||||
|
timer::update_bpm_timers,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
src/tests/attack.rs
Normal file
26
src/tests/attack.rs
Normal file
|
|
@ -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.);
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
mod attack;
|
||||||
mod graph;
|
mod graph;
|
||||||
|
|
|
||||||
85
src/timer.rs
Normal file
85
src/timer.rs
Normal file
|
|
@ -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<Time>, timers: Query<(Entity, &mut BpmTimer)>) {
|
||||||
|
for (timer_id, mut timer) in timers {
|
||||||
|
if timer.tick(time.delta()) {
|
||||||
|
commands.trigger(TickEvent { timer: timer_id, elapsed: timer.beats_elapsed() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue