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)]
|
||||
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<AssetServer>) {
|
||||
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(),
|
||||
))
|
||||
.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;
|
||||
|
|
|
|||
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