generated from 2ndbeam/bevy-template
feat: Player attacking
- Added BpmTimer.accuracy method - Added Weapon.attack_offset method - Replaced beats_remaining with timer in Attacking and Awaiting - AttackArea spawning on Attacking end
This commit is contained in:
parent
95276c070b
commit
fb1923fe53
9 changed files with 107 additions and 58 deletions
|
|
@ -37,6 +37,5 @@ pub const GROUP_ATTACK: Group = Group::GROUP_5;
|
||||||
/// 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.))
|
commands.spawn(BpmTimer::new(120.));
|
||||||
.observe(player::systems::on_timer_tick);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,10 @@ impl Player {
|
||||||
// Remove NextAttack before Attacking
|
// Remove NextAttack before Attacking
|
||||||
.on_enter::<states::Attacking>(|c| { c.remove::<states::NextAttack>(); } )
|
.on_enter::<states::Attacking>(|c| { c.remove::<states::NextAttack>(); } )
|
||||||
// Attacking -> Awaiting
|
// Attacking -> Awaiting
|
||||||
.trans::<states::Attacking, _>(done(None), states::Awaiting { beats_remaining: 1 })
|
.trans_builder(
|
||||||
|
done(None).ignore_and(triggers::get_timer_bpm),
|
||||||
|
triggers::from_attacking_to_awaiting,
|
||||||
|
)
|
||||||
// Awaiting -> Free
|
// Awaiting -> Free
|
||||||
.trans::<states::Awaiting, _>(done(None), states::Free)
|
.trans::<states::Awaiting, _>(done(None), states::Free)
|
||||||
// Awaiting -> Choosing
|
// Awaiting -> Choosing
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::fmt::Debug;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{graph::action::ActionNode, input::PlayerInput, player::weapon::AttackType};
|
use crate::{graph::action::ActionNode, input::PlayerInput, player::weapon::AttackType, timer::duration_from_bpm};
|
||||||
|
|
||||||
/// Player is not doing anything special
|
/// Player is not doing anything special
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
|
|
@ -22,26 +22,44 @@ pub struct Choosing {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Player is acting and cannot do something else
|
/// Player is acting and cannot do something else
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
|
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
|
||||||
#[reflect(Component, Clone, PartialEq, Debug)]
|
#[reflect(Component, Clone, PartialEq, Debug)]
|
||||||
#[component(storage = "SparseSet")]
|
#[component(storage = "SparseSet")]
|
||||||
pub struct Attacking {
|
pub struct Attacking {
|
||||||
/// Which physical action was chosen
|
/// Which physical action was chosen
|
||||||
pub phys: ActionNode<AttackType>,
|
pub phys: ActionNode<AttackType>,
|
||||||
/// How much time left before action ends
|
/// Timer that awaits for attack to end
|
||||||
pub beats_remaining: u32,
|
pub timer: Timer,
|
||||||
|
/// How precisely user clicked in the beat
|
||||||
|
pub accuracy: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attacking {
|
||||||
|
/// Constructs new [Attacking] from given beats and bpm
|
||||||
|
pub fn new(phys: ActionNode<AttackType>, accuracy: f32, beats: f32, bpm: f32) -> Self {
|
||||||
|
let duration = duration_from_bpm(bpm).mul_f32(beats);
|
||||||
|
Self { phys, timer: Timer::new(duration, TimerMode::Once), accuracy }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Next attack to be performed
|
/// Next attack to be performed
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
|
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
|
||||||
#[reflect(Component, Clone, PartialEq, Debug)]
|
#[reflect(Component, Clone, PartialEq, Debug)]
|
||||||
pub struct NextAttack(pub Attacking);
|
pub struct NextAttack(pub Attacking);
|
||||||
|
|
||||||
/// Player is awaiting next action
|
/// Player is awaiting next action
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
#[derive(Component, Clone, PartialEq, Debug, Default, Reflect)]
|
||||||
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
#[component(storage = "SparseSet")]
|
#[component(storage = "SparseSet")]
|
||||||
pub struct Awaiting {
|
pub struct Awaiting {
|
||||||
/// How much time left before transitioning to Free
|
/// Timer that awaits for next action
|
||||||
pub beats_remaining: u32,
|
pub timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Awaiting {
|
||||||
|
/// Constructs new [Awaiting] from given beats and bpm
|
||||||
|
pub fn new(beats: f32, bpm: f32) -> Self {
|
||||||
|
let duration = duration_from_bpm(bpm).mul_f32(beats);
|
||||||
|
Self { timer: Timer::new(duration, TimerMode::Once) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
//! Player systems
|
//! Player systems
|
||||||
|
|
||||||
use crate::timer::TickEvent;
|
use bevy_trait_query::One;
|
||||||
|
|
||||||
|
use crate::timer::BpmTimer;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
@ -17,19 +19,33 @@ pub fn handle_input(
|
||||||
&mut KinematicCharacterController,
|
&mut KinematicCharacterController,
|
||||||
&mut Sprite,
|
&mut Sprite,
|
||||||
Option<&mut AttackGraph>,
|
Option<&mut AttackGraph>,
|
||||||
|
Option<One<&dyn Weapon>>,
|
||||||
Option<&states::Free>,
|
Option<&states::Free>,
|
||||||
Option<&states::Choosing>,
|
Option<&states::Choosing>,
|
||||||
Option<&states::Attacking>,
|
Option<&mut states::Attacking>,
|
||||||
Option<&states::Awaiting>,
|
Option<&mut states::Awaiting>,
|
||||||
)>,
|
)>,
|
||||||
|
|
||||||
|
timer_query: Query<&BpmTimer>,
|
||||||
) {
|
) {
|
||||||
|
let Some(timer) = timer_query.iter().next() else {
|
||||||
|
error!("No BpmTimer provided");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bpm = timer.get_bpm();
|
||||||
|
|
||||||
for (
|
for (
|
||||||
|
// Basic things
|
||||||
player_id,
|
player_id,
|
||||||
player,
|
player,
|
||||||
action_state,
|
action_state,
|
||||||
mut controller,
|
mut controller,
|
||||||
mut sprite,
|
mut sprite,
|
||||||
|
// Weapon
|
||||||
maybe_attack_graph,
|
maybe_attack_graph,
|
||||||
|
maybe_weapon,
|
||||||
|
// States
|
||||||
maybe_free,
|
maybe_free,
|
||||||
maybe_choosing,
|
maybe_choosing,
|
||||||
maybe_attacking,
|
maybe_attacking,
|
||||||
|
|
@ -46,18 +62,13 @@ pub fn handle_input(
|
||||||
} else if let Some(states::Choosing { log }) = maybe_choosing &&
|
} else if let Some(states::Choosing { log }) = maybe_choosing &&
|
||||||
let Some(mut attack_graph) = maybe_attack_graph {
|
let Some(mut attack_graph) = maybe_attack_graph {
|
||||||
if let Some(next_state) = attack_graph.next(*log) {
|
if let Some(next_state) = attack_graph.next(*log) {
|
||||||
|
let accuracy = timer.accuracy();
|
||||||
let next_attack = match *log {
|
let next_attack = match *log {
|
||||||
PlayerInput::LightAttack => {
|
PlayerInput::LightAttack => {
|
||||||
states::Attacking {
|
states::Attacking::new(next_state, accuracy, 1., bpm)
|
||||||
phys: next_state,
|
|
||||||
beats_remaining: 1,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
PlayerInput::HeavyAttack => {
|
PlayerInput::HeavyAttack => {
|
||||||
states::Attacking {
|
states::Attacking::new(next_state, accuracy, 2., bpm)
|
||||||
phys: next_state,
|
|
||||||
beats_remaining: 2,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
@ -65,35 +76,28 @@ pub fn handle_input(
|
||||||
} else {
|
} else {
|
||||||
commands.entity(player_id).insert(Done::Failure);
|
commands.entity(player_id).insert(Done::Failure);
|
||||||
}
|
}
|
||||||
} else if let Some(states::Attacking { phys, beats_remaining }) = maybe_attacking {
|
} else if let Some(mut attacking) = maybe_attacking {
|
||||||
println!("{phys:#?}");
|
attacking.timer.tick(time.delta());
|
||||||
if *beats_remaining == 0 {
|
|
||||||
commands.entity(player_id).insert(Done::Success);
|
|
||||||
}
|
|
||||||
} else if let Some(states::Awaiting { beats_remaining, .. }) = maybe_awaiting {
|
|
||||||
if *beats_remaining == 0 {
|
|
||||||
commands.entity(player_id).insert(Done::Success);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change beats_remaining to more accurate counting
|
if attacking.timer.just_finished() {
|
||||||
/// Observer that updates temporal states on timer tick
|
info!("{}", attacking.accuracy);
|
||||||
pub fn on_timer_tick(
|
commands.entity(player_id).insert(Done::Success);
|
||||||
_: On<TickEvent>,
|
let Some(weapon) = maybe_weapon else {
|
||||||
player_query: Query<(
|
error!("No weapon provided for player");
|
||||||
Option<&mut states::Attacking>,
|
return;
|
||||||
Option<&mut states::Awaiting>,
|
};
|
||||||
), With<Player>>
|
|
||||||
) {
|
if let Some(attack_area) = weapon.attack_area(attacking.phys, attacking.accuracy) {
|
||||||
for (maybe_attacking, maybe_awaiting) in player_query {
|
let offset = weapon.attack_offset();
|
||||||
if let Some(mut attacking) = maybe_attacking {
|
commands.entity(player_id)
|
||||||
info!("attack tick");
|
.with_child(attack_area.into_bundle(offset, sprite.flip_x, GROUP_ENEMY));
|
||||||
attacking.beats_remaining -= 1;
|
}
|
||||||
|
}
|
||||||
} else if let Some(mut awaiting) = maybe_awaiting {
|
} else if let Some(mut awaiting) = maybe_awaiting {
|
||||||
info!("await tick");
|
awaiting.timer.tick(time.delta());
|
||||||
awaiting.beats_remaining -= 1;
|
if awaiting.timer.just_finished() {
|
||||||
|
commands.entity(player_id).insert(Done::Success);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
//! Player state machine triggers
|
//! Player state machine triggers
|
||||||
|
|
||||||
|
use crate::timer::BpmTimer;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use super::states::*;
|
use super::states::*;
|
||||||
|
|
||||||
/// Returns an action of any of given actions are just pressed
|
/// Returns an action of any of given actions are just pressed
|
||||||
pub fn any_action_just_pressed(actions: Vec<PlayerInput>) -> impl EntityTrigger<Out = Option<PlayerInput>> {
|
pub fn any_action_just_pressed(actions: Vec<PlayerInput>) -> impl EntityTrigger<Out = Option<PlayerInput>> {
|
||||||
(move |In(player_id): In<Entity>, query: Query<&InputState, With<Player>>| {
|
(move |In(player_id): In<Entity>, query: Query<&InputState, With<Player>>| {
|
||||||
let Ok(input) = query.get(player_id) else {
|
let Ok(input) = query.get(player_id) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -27,15 +29,25 @@ pub fn from_free_to_choosing(trans: Trans<Free, PlayerInput>) -> Choosing {
|
||||||
/// Returns true if player has [NextAttack]
|
/// Returns true if player has [NextAttack]
|
||||||
pub fn attack_queued(In(player_id): In<Entity>, query: Query<&NextAttack>) -> Option<Attacking> {
|
pub fn attack_queued(In(player_id): In<Entity>, query: Query<&NextAttack>) -> Option<Attacking> {
|
||||||
match query.get(player_id) {
|
match query.get(player_id) {
|
||||||
Ok(next) => Some(next.0),
|
Ok(next) => Some(next.0.clone()),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transition from [Choosing] to [Attacking]
|
/// Transition from [Choosing] to [Attacking]
|
||||||
pub fn from_choosing_to_attacking(trans: Trans<Choosing, Attacking>) -> Attacking { trans.out }
|
pub fn from_choosing_to_attacking(trans: Trans<Choosing, Attacking>) -> Attacking { trans.out.clone() }
|
||||||
|
|
||||||
/// Transition from [Awaiting] to [Choosing]
|
/// Transition from [Awaiting] to [Choosing]
|
||||||
pub fn from_awaiting_to_choosing(trans: Trans<Awaiting, PlayerInput>) -> Choosing {
|
pub fn from_awaiting_to_choosing(trans: Trans<Awaiting, PlayerInput>) -> Choosing {
|
||||||
Choosing { log: trans.out }
|
Choosing { log: trans.out }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trigger that returns [BpmTimer]'s bpm
|
||||||
|
pub fn get_timer_bpm(query: Query<&BpmTimer>) -> Option<f32> {
|
||||||
|
query.iter().next().map(|t| t.get_bpm())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition from [Attacking] to [Awaiting]
|
||||||
|
pub fn from_attacking_to_awaiting(trans: Trans<Attacking, f32>) -> Awaiting {
|
||||||
|
Awaiting::new(2., trans.out)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl Weapon for Knife {
|
||||||
ActionGraph::from_paths(paths)
|
ActionGraph::from_paths(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attack_bundle(&self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea> {
|
fn attack_area(&self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea> {
|
||||||
let Some(action_type) = attack.action else {
|
let Some(action_type) = attack.action else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -57,27 +57,31 @@ impl Weapon for Knife {
|
||||||
|
|
||||||
let (duration, half_area) = match action_type {
|
let (duration, half_area) = match action_type {
|
||||||
AttackType::HorizontalSwing => {(
|
AttackType::HorizontalSwing => {(
|
||||||
Duration::from_secs(1),
|
Duration::from_millis(500),
|
||||||
vec2(meters(0.5), meters(0.2)),
|
vec2(meters(0.5), meters(0.2)),
|
||||||
)},
|
)},
|
||||||
AttackType::VerticalSwing => {(
|
AttackType::VerticalSwing => {(
|
||||||
Duration::from_millis(1_500),
|
Duration::from_millis(500),
|
||||||
vec2(meters(0.5), meters(1.)),
|
vec2(meters(0.5), meters(0.5)),
|
||||||
)},
|
)},
|
||||||
AttackType::RoundSwing => {(
|
AttackType::RoundSwing => {(
|
||||||
Duration::from_secs(1),
|
Duration::from_millis(500),
|
||||||
vec2(meters(0.5), meters(0.2)),
|
vec2(meters(0.5), meters(0.2)),
|
||||||
)},
|
)},
|
||||||
AttackType::Stab => {(
|
AttackType::Stab => {(
|
||||||
Duration::from_secs(1),
|
Duration::from_millis(500),
|
||||||
vec2(meters(0.5), meters(0.1)),
|
vec2(meters(0.5), meters(0.1)),
|
||||||
)},
|
)},
|
||||||
AttackType::Lunge => {(
|
AttackType::Lunge => {(
|
||||||
Duration::from_millis(1_500),
|
Duration::from_millis(500),
|
||||||
vec2(meters(0.7), meters(0.1)),
|
vec2(meters(0.7), meters(0.1)),
|
||||||
)},
|
)},
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(AttackArea::new(accuracy, damage, duration, half_area))
|
Some(AttackArea::new(accuracy, damage, duration, half_area))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attack_offset(self: &Self) -> Vec2 {
|
||||||
|
vec2(meters(0.5), 0.)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,8 @@ pub trait Weapon {
|
||||||
fn graph(self: &Self) -> ActionGraph<AttackType, PlayerInput>;
|
fn graph(self: &Self) -> ActionGraph<AttackType, PlayerInput>;
|
||||||
|
|
||||||
/// Returns [AttackArea] for given attack
|
/// Returns [AttackArea] for given attack
|
||||||
fn attack_bundle(self: &Self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea>;
|
fn attack_area(self: &Self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea>;
|
||||||
|
|
||||||
|
/// Returns [AttackArea] offset for this weapon
|
||||||
|
fn attack_offset(self: &Self) -> Vec2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ impl Plugin for GamePlugin {
|
||||||
))
|
))
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
|
combat::attack::update_attack_areas,
|
||||||
player::systems::handle_input,
|
player::systems::handle_input,
|
||||||
timer::update_bpm_timers,
|
timer::update_bpm_timers,
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,11 @@ impl BpmTimer {
|
||||||
self.timer.reset();
|
self.timer.reset();
|
||||||
self.elapsed = 0.;
|
self.elapsed = 0.;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns how close current state is to rhythm
|
||||||
|
pub fn accuracy(&self) -> f32 {
|
||||||
|
(self.timer.fraction() - 0.5).abs() * 2.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System that ticks each [BpmTimer] and triggers [TickEvent]s
|
/// System that ticks each [BpmTimer] and triggers [TickEvent]s
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue