From fb1923fe532215da322e592ef50044ac0b9cec6b Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 17 Apr 2026 12:53:20 +0300 Subject: [PATCH] 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 --- src/lib.rs | 3 +- src/player/mod.rs | 5 ++- src/player/states.rs | 34 ++++++++++++---- src/player/systems.rs | 80 ++++++++++++++++++++------------------ src/player/triggers.rs | 18 +++++++-- src/player/weapon/knife.rs | 18 +++++---- src/player/weapon/mod.rs | 5 ++- src/plugin.rs | 1 + src/timer.rs | 5 +++ 9 files changed, 109 insertions(+), 60 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fba163b..bdf01d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,5 @@ pub const GROUP_ATTACK: Group = Group::GROUP_5; /// 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.)) - .observe(player::systems::on_timer_tick); + commands.spawn(BpmTimer::new(120.)); } diff --git a/src/player/mod.rs b/src/player/mod.rs index 6c7e446..803a083 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -90,7 +90,10 @@ impl Player { // Remove NextAttack before Attacking .on_enter::(|c| { c.remove::(); } ) // Attacking -> Awaiting - .trans::(done(None), states::Awaiting { beats_remaining: 1 }) + .trans_builder( + done(None).ignore_and(triggers::get_timer_bpm), + triggers::from_attacking_to_awaiting, + ) // Awaiting -> Free .trans::(done(None), states::Free) // Awaiting -> Choosing diff --git a/src/player/states.rs b/src/player/states.rs index 7991e2b..d709224 100644 --- a/src/player/states.rs +++ b/src/player/states.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; 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 #[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)] @@ -22,26 +22,44 @@ pub struct Choosing { } /// 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)] #[component(storage = "SparseSet")] pub struct Attacking { /// Which physical action was chosen pub phys: ActionNode, - /// How much time left before action ends - pub beats_remaining: u32, + /// Timer that awaits for attack to end + 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, 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 -#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect)] +#[derive(Component, Clone, PartialEq, Debug, Reflect)] #[reflect(Component, Clone, PartialEq, Debug)] pub struct NextAttack(pub Attacking); /// 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)] #[component(storage = "SparseSet")] pub struct Awaiting { - /// How much time left before transitioning to Free - pub beats_remaining: u32, + /// Timer that awaits for next action + 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) } + } } diff --git a/src/player/systems.rs b/src/player/systems.rs index 3274a08..89b3fe9 100644 --- a/src/player/systems.rs +++ b/src/player/systems.rs @@ -1,6 +1,8 @@ //! Player systems -use crate::timer::TickEvent; +use bevy_trait_query::One; + +use crate::timer::BpmTimer; use super::*; @@ -17,19 +19,33 @@ pub fn handle_input( &mut KinematicCharacterController, &mut Sprite, Option<&mut AttackGraph>, + Option>, Option<&states::Free>, Option<&states::Choosing>, - Option<&states::Attacking>, - Option<&states::Awaiting>, + Option<&mut states::Attacking>, + 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 ( + // Basic things player_id, player, action_state, mut controller, mut sprite, + // Weapon maybe_attack_graph, + maybe_weapon, + // States maybe_free, maybe_choosing, maybe_attacking, @@ -46,18 +62,13 @@ pub fn handle_input( } else if let Some(states::Choosing { log }) = maybe_choosing && let Some(mut attack_graph) = maybe_attack_graph { if let Some(next_state) = attack_graph.next(*log) { + let accuracy = timer.accuracy(); let next_attack = match *log { PlayerInput::LightAttack => { - states::Attacking { - phys: next_state, - beats_remaining: 1, - } + states::Attacking::new(next_state, accuracy, 1., bpm) }, PlayerInput::HeavyAttack => { - states::Attacking { - phys: next_state, - beats_remaining: 2, - } + states::Attacking::new(next_state, accuracy, 2., bpm) }, _ => unreachable!(), }; @@ -65,35 +76,28 @@ pub fn handle_input( } else { commands.entity(player_id).insert(Done::Failure); } - } else if let Some(states::Attacking { phys, beats_remaining }) = maybe_attacking { - println!("{phys:#?}"); - 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); - } - } - } -} + } else if let Some(mut attacking) = maybe_attacking { + attacking.timer.tick(time.delta()); -// TODO: change beats_remaining to more accurate counting -/// Observer that updates temporal states on timer tick -pub fn on_timer_tick( - _: On, - player_query: Query<( - Option<&mut states::Attacking>, - Option<&mut states::Awaiting>, - ), With> -) { - for (maybe_attacking, maybe_awaiting) in player_query { - if let Some(mut attacking) = maybe_attacking { - info!("attack tick"); - attacking.beats_remaining -= 1; + if attacking.timer.just_finished() { + info!("{}", attacking.accuracy); + commands.entity(player_id).insert(Done::Success); + let Some(weapon) = maybe_weapon else { + error!("No weapon provided for player"); + return; + }; + + if let Some(attack_area) = weapon.attack_area(attacking.phys, attacking.accuracy) { + let offset = weapon.attack_offset(); + commands.entity(player_id) + .with_child(attack_area.into_bundle(offset, sprite.flip_x, GROUP_ENEMY)); + } + } } else if let Some(mut awaiting) = maybe_awaiting { - info!("await tick"); - awaiting.beats_remaining -= 1; + awaiting.timer.tick(time.delta()); + if awaiting.timer.just_finished() { + commands.entity(player_id).insert(Done::Success); + } } } } diff --git a/src/player/triggers.rs b/src/player/triggers.rs index 2a9588d..16e5b03 100644 --- a/src/player/triggers.rs +++ b/src/player/triggers.rs @@ -1,11 +1,13 @@ //! Player state machine triggers +use crate::timer::BpmTimer; + use super::*; use super::states::*; /// Returns an action of any of given actions are just pressed pub fn any_action_just_pressed(actions: Vec) -> impl EntityTrigger> { - (move |In(player_id): In, query: Query<&InputState, With>| { + (move |In(player_id): In, query: Query<&InputState, With>| { let Ok(input) = query.get(player_id) else { return None; }; @@ -27,15 +29,25 @@ pub fn from_free_to_choosing(trans: Trans) -> Choosing { /// Returns true if player has [NextAttack] pub fn attack_queued(In(player_id): In, query: Query<&NextAttack>) -> Option { match query.get(player_id) { - Ok(next) => Some(next.0), + Ok(next) => Some(next.0.clone()), Err(_) => None, } } /// Transition from [Choosing] to [Attacking] -pub fn from_choosing_to_attacking(trans: Trans) -> Attacking { trans.out } +pub fn from_choosing_to_attacking(trans: Trans) -> Attacking { trans.out.clone() } /// Transition from [Awaiting] to [Choosing] pub fn from_awaiting_to_choosing(trans: Trans) -> Choosing { Choosing { log: trans.out } } + +/// Trigger that returns [BpmTimer]'s bpm +pub fn get_timer_bpm(query: Query<&BpmTimer>) -> Option { + query.iter().next().map(|t| t.get_bpm()) +} + +/// Transition from [Attacking] to [Awaiting] +pub fn from_attacking_to_awaiting(trans: Trans) -> Awaiting { + Awaiting::new(2., trans.out) +} diff --git a/src/player/weapon/knife.rs b/src/player/weapon/knife.rs index caa48cb..28779fc 100644 --- a/src/player/weapon/knife.rs +++ b/src/player/weapon/knife.rs @@ -44,7 +44,7 @@ impl Weapon for Knife { ActionGraph::from_paths(paths) } - fn attack_bundle(&self, attack: ActionNode, accuracy: f32) -> Option { + fn attack_area(&self, attack: ActionNode, accuracy: f32) -> Option { let Some(action_type) = attack.action else { return None; }; @@ -57,27 +57,31 @@ impl Weapon for Knife { let (duration, half_area) = match action_type { AttackType::HorizontalSwing => {( - Duration::from_secs(1), + Duration::from_millis(500), vec2(meters(0.5), meters(0.2)), )}, AttackType::VerticalSwing => {( - Duration::from_millis(1_500), - vec2(meters(0.5), meters(1.)), + Duration::from_millis(500), + vec2(meters(0.5), meters(0.5)), )}, AttackType::RoundSwing => {( - Duration::from_secs(1), + Duration::from_millis(500), vec2(meters(0.5), meters(0.2)), )}, AttackType::Stab => {( - Duration::from_secs(1), + Duration::from_millis(500), vec2(meters(0.5), meters(0.1)), )}, AttackType::Lunge => {( - Duration::from_millis(1_500), + Duration::from_millis(500), vec2(meters(0.7), meters(0.1)), )}, }; Some(AttackArea::new(accuracy, damage, duration, half_area)) } + + fn attack_offset(self: &Self) -> Vec2 { + vec2(meters(0.5), 0.) + } } diff --git a/src/player/weapon/mod.rs b/src/player/weapon/mod.rs index 9838dca..b28c185 100644 --- a/src/player/weapon/mod.rs +++ b/src/player/weapon/mod.rs @@ -30,5 +30,8 @@ pub trait Weapon { fn graph(self: &Self) -> ActionGraph; /// Returns [AttackArea] for given attack - fn attack_bundle(self: &Self, attack: ActionNode, accuracy: f32) -> Option; + fn attack_area(self: &Self, attack: ActionNode, accuracy: f32) -> Option; + + /// Returns [AttackArea] offset for this weapon + fn attack_offset(self: &Self) -> Vec2; } diff --git a/src/plugin.rs b/src/plugin.rs index 0ba14d8..c408a8d 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -25,6 +25,7 @@ impl Plugin for GamePlugin { )) .add_systems(Startup, setup) .add_systems(Update, ( + combat::attack::update_attack_areas, player::systems::handle_input, timer::update_bpm_timers, )) diff --git a/src/timer.rs b/src/timer.rs index cfe23e1..7849032 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -73,6 +73,11 @@ impl BpmTimer { self.timer.reset(); 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