generated from 2ndbeam/bevy-template
feat: Proof-of-concept attack combo system
This commit is contained in:
parent
88a73275ff
commit
95276c070b
15 changed files with 391 additions and 60 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
|
@ -356,6 +356,30 @@ dependencies = [
|
||||||
"bevy_internal",
|
"bevy_internal",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bevy-trait-query"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40775521e471d7bee8e5571ac706ed4d2df896a85242201f8491eb0d02a8e44a"
|
||||||
|
dependencies = [
|
||||||
|
"bevy-trait-query-impl",
|
||||||
|
"bevy_app",
|
||||||
|
"bevy_ecs",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bevy-trait-query-impl"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec6ac32b12f325ab3781e8998c1f8e99e7ec6f73e934988427fde68e324b751d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy_a11y"
|
name = "bevy_a11y"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
|
|
@ -588,6 +612,8 @@ name = "bevy_combat_proto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy",
|
"bevy",
|
||||||
|
"bevy-trait-query",
|
||||||
|
"bevy_ecs",
|
||||||
"bevy_rapier2d",
|
"bevy_rapier2d",
|
||||||
"leafwing-input-manager",
|
"leafwing-input-manager",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.18.0", features = ["bevy_remote"] }
|
bevy = { version = "0.18.0", features = ["bevy_remote"] }
|
||||||
|
bevy-trait-query = "0.18.0"
|
||||||
|
bevy_ecs = "0.18.1"
|
||||||
bevy_rapier2d = "0.33.0"
|
bevy_rapier2d = "0.33.0"
|
||||||
leafwing-input-manager = "0.20.0"
|
leafwing-input-manager = "0.20.0"
|
||||||
petgraph = { version = "0.8.3" }
|
petgraph = { version = "0.8.3" }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub struct AttackArea {
|
||||||
accuracy: f32,
|
accuracy: f32,
|
||||||
damage: f32,
|
damage: f32,
|
||||||
timer: Timer,
|
timer: Timer,
|
||||||
|
half_area: Vec2,
|
||||||
max_distance: f32,
|
max_distance: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,24 +33,23 @@ impl AttackArea {
|
||||||
accuracy,
|
accuracy,
|
||||||
damage,
|
damage,
|
||||||
timer: Timer::new(duration, TimerMode::Once),
|
timer: Timer::new(duration, TimerMode::Once),
|
||||||
|
half_area,
|
||||||
max_distance,
|
max_distance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns attack area bundle with everything needed
|
/// Returns attack area bundle with everything needed
|
||||||
pub fn bundle(
|
pub fn into_bundle(
|
||||||
damage: f32,
|
self,
|
||||||
accuracy: f32,
|
|
||||||
duration: Duration,
|
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
half_area: Vec2,
|
|
||||||
facing_left: bool,
|
facing_left: bool,
|
||||||
affinity: Group,
|
affinity: Group,
|
||||||
) -> impl Bundle {
|
) -> impl Bundle {
|
||||||
|
let half_area = self.half_area;
|
||||||
let origin_x = if facing_left { position.x - half_area.x } else { position.x + half_area.x };
|
let origin_x = if facing_left { position.x - half_area.x } else { position.x + half_area.x };
|
||||||
(
|
(
|
||||||
// Basic
|
// Basic
|
||||||
AttackArea::new(accuracy, damage, duration, half_area),
|
self,
|
||||||
|
|
||||||
// Collision
|
// Collision
|
||||||
Transform::from_xyz(position.x, position.y, 0.),
|
Transform::from_xyz(position.x, position.y, 0.),
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ pub trait PhysicalAction<Log: Eq>: NodeTrait {
|
||||||
|
|
||||||
/// Graph that defines relations between physical actions
|
/// Graph that defines relations between physical actions
|
||||||
/// See module docs for usage example
|
/// See module docs for usage example
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Component, Clone, Debug)]
|
||||||
pub struct ActionGraph<Phys, Log>
|
pub struct ActionGraph<Phys, Log>
|
||||||
where Phys: NodeTrait, Log: Eq {
|
where Phys: NodeTrait, Log: Eq {
|
||||||
graph: DiGraphMap<ActionNode<Phys>, Log>,
|
graph: DiGraphMap<ActionNode<Phys>, Log>,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@ impl DefaultInputMap for PlayerInput {
|
||||||
.with(Self::DodgeBlock, GamepadButton::South)
|
.with(Self::DodgeBlock, GamepadButton::South)
|
||||||
|
|
||||||
.with(Self::LightAttack, MouseButton::Left)
|
.with(Self::LightAttack, MouseButton::Left)
|
||||||
.with(Self::HeavyAttack, GamepadButton::North)
|
.with(Self::LightAttack, GamepadButton::North)
|
||||||
|
|
||||||
.with(Self::LightAttack, MouseButton::Right)
|
.with(Self::HeavyAttack, MouseButton::Right)
|
||||||
.with(Self::HeavyAttack, GamepadButton::East)
|
.with(Self::HeavyAttack, GamepadButton::East)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ pub mod graph;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod states;
|
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
@ -38,5 +37,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,22 @@
|
||||||
use bevy::{camera::ScalingMode, prelude::*};
|
use bevy::{camera::ScalingMode, prelude::*};
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
use leafwing_input_manager::prelude::*;
|
use leafwing_input_manager::prelude::*;
|
||||||
|
use seldom_state::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, input::{
|
GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, graph::action::ActionGraph, input::{
|
||||||
DefaultInputMap,
|
DefaultInputMap,
|
||||||
PlayerInput,
|
PlayerInput,
|
||||||
}, meters
|
}, meters, player::weapon::{AttackType, Weapon}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod states;
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
|
pub mod triggers;
|
||||||
|
pub mod weapon;
|
||||||
|
|
||||||
|
type AttackGraph = ActionGraph<AttackType, PlayerInput>;
|
||||||
|
type InputState = ActionState<PlayerInput>;
|
||||||
|
|
||||||
/// Player component
|
/// Player component
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect)]
|
#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect)]
|
||||||
|
|
@ -47,6 +54,7 @@ impl Player {
|
||||||
PlayerInput::input_map(),
|
PlayerInput::input_map(),
|
||||||
|
|
||||||
// Collision
|
// Collision
|
||||||
|
(
|
||||||
RigidBody::KinematicPositionBased,
|
RigidBody::KinematicPositionBased,
|
||||||
KinematicCharacterController::default(),
|
KinematicCharacterController::default(),
|
||||||
ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC,
|
ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC,
|
||||||
|
|
@ -57,9 +65,47 @@ impl Player {
|
||||||
Collider::cuboid(meters(0.3), meters(0.9)),
|
Collider::cuboid(meters(0.3), meters(0.9)),
|
||||||
ActiveEvents::COLLISION_EVENTS,
|
ActiveEvents::COLLISION_EVENTS,
|
||||||
Sleeping::disabled(),
|
Sleeping::disabled(),
|
||||||
|
),
|
||||||
|
|
||||||
// State
|
// State Machine
|
||||||
crate::states::Free,
|
states::Free,
|
||||||
|
StateMachine::default()
|
||||||
|
// Reset graph before Free
|
||||||
|
.on_enter::<states::Free>(|c| { c.entry::<AttackGraph>().and_modify(|mut g| g.reset()); })
|
||||||
|
// Free -> Choosing
|
||||||
|
.trans_builder(
|
||||||
|
triggers::any_action_just_pressed(vec![
|
||||||
|
PlayerInput::HeavyAttack,
|
||||||
|
PlayerInput::LightAttack,
|
||||||
|
]),
|
||||||
|
triggers::from_free_to_choosing,
|
||||||
|
)
|
||||||
|
// Choosing -> Free
|
||||||
|
.trans::<states::Choosing, _>(done(None), states::Free)
|
||||||
|
// Choosing -> Attacking
|
||||||
|
.trans_builder(
|
||||||
|
triggers::attack_queued,
|
||||||
|
triggers::from_choosing_to_attacking,
|
||||||
|
)
|
||||||
|
// Remove NextAttack before Attacking
|
||||||
|
.on_enter::<states::Attacking>(|c| { c.remove::<states::NextAttack>(); } )
|
||||||
|
// Attacking -> Awaiting
|
||||||
|
.trans::<states::Attacking, _>(done(None), states::Awaiting { beats_remaining: 1 })
|
||||||
|
// Awaiting -> Free
|
||||||
|
.trans::<states::Awaiting, _>(done(None), states::Free)
|
||||||
|
// Awaiting -> Choosing
|
||||||
|
.trans_builder(
|
||||||
|
triggers::any_action_just_pressed(vec![
|
||||||
|
PlayerInput::HeavyAttack,
|
||||||
|
PlayerInput::LightAttack,
|
||||||
|
]),
|
||||||
|
triggers::from_awaiting_to_choosing,
|
||||||
|
)
|
||||||
|
.set_trans_logging(true),
|
||||||
|
|
||||||
|
// Weapon
|
||||||
|
weapon::knife::Knife::default(),
|
||||||
|
weapon::knife::Knife::default().graph(),
|
||||||
|
|
||||||
Children::spawn((
|
Children::spawn((
|
||||||
// Camera
|
// Camera
|
||||||
|
|
|
||||||
47
src/player/states.rs
Normal file
47
src/player/states.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
//! Commonly used entity states
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{graph::action::ActionNode, input::PlayerInput, player::weapon::AttackType};
|
||||||
|
|
||||||
|
/// Player is not doing anything special
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
#[component(storage = "SparseSet")]
|
||||||
|
pub struct Free;
|
||||||
|
|
||||||
|
/// Player is choosing next action
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug)]
|
||||||
|
#[component(storage = "SparseSet")]
|
||||||
|
pub struct Choosing {
|
||||||
|
/// Which logical action was sent
|
||||||
|
pub log: PlayerInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Player is acting and cannot do something else
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug)]
|
||||||
|
#[component(storage = "SparseSet")]
|
||||||
|
pub struct Attacking {
|
||||||
|
/// Which physical action was chosen
|
||||||
|
pub phys: ActionNode<AttackType>,
|
||||||
|
/// How much time left before action ends
|
||||||
|
pub beats_remaining: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next attack to be performed
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, 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)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
#[component(storage = "SparseSet")]
|
||||||
|
pub struct Awaiting {
|
||||||
|
/// How much time left before transitioning to Free
|
||||||
|
pub beats_remaining: u32,
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,41 @@
|
||||||
//! Player systems
|
//! Player systems
|
||||||
|
|
||||||
|
use crate::timer::TickEvent;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Do something based on [PlayerInput]
|
/// Do something based on [PlayerInput]
|
||||||
pub fn handle_input(
|
pub fn handle_input(
|
||||||
|
mut commands: Commands,
|
||||||
|
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
|
|
||||||
player_query: Query<(
|
player_query: Query<(
|
||||||
|
Entity,
|
||||||
&Player,
|
&Player,
|
||||||
&ActionState<PlayerInput>,
|
&InputState,
|
||||||
&mut KinematicCharacterController,
|
&mut KinematicCharacterController,
|
||||||
&mut Sprite,
|
&mut Sprite,
|
||||||
|
Option<&mut AttackGraph>,
|
||||||
|
Option<&states::Free>,
|
||||||
|
Option<&states::Choosing>,
|
||||||
|
Option<&states::Attacking>,
|
||||||
|
Option<&states::Awaiting>,
|
||||||
)>,
|
)>,
|
||||||
) {
|
) {
|
||||||
for (player, action_state, mut controller, mut sprite) in player_query {
|
for (
|
||||||
|
player_id,
|
||||||
|
player,
|
||||||
|
action_state,
|
||||||
|
mut controller,
|
||||||
|
mut sprite,
|
||||||
|
maybe_attack_graph,
|
||||||
|
maybe_free,
|
||||||
|
maybe_choosing,
|
||||||
|
maybe_attacking,
|
||||||
|
maybe_awaiting,
|
||||||
|
) in player_query {
|
||||||
|
if maybe_free.is_some() {
|
||||||
let direction = action_state.clamped_value(&PlayerInput::Move);
|
let direction = action_state.clamped_value(&PlayerInput::Move);
|
||||||
|
|
||||||
controller.translation = Some(vec2(direction * player.speed * time.delta_secs(), 0.));
|
controller.translation = Some(vec2(direction * player.speed * time.delta_secs(), 0.));
|
||||||
|
|
@ -21,5 +43,57 @@ pub fn handle_input(
|
||||||
if direction != 0. {
|
if direction != 0. {
|
||||||
sprite.flip_x = direction < 0.;
|
sprite.flip_x = direction < 0.;
|
||||||
}
|
}
|
||||||
|
} 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 next_attack = match *log {
|
||||||
|
PlayerInput::LightAttack => {
|
||||||
|
states::Attacking {
|
||||||
|
phys: next_state,
|
||||||
|
beats_remaining: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PlayerInput::HeavyAttack => {
|
||||||
|
states::Attacking {
|
||||||
|
phys: next_state,
|
||||||
|
beats_remaining: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
commands.entity(player_id).insert(states::NextAttack(next_attack));
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change beats_remaining to more accurate counting
|
||||||
|
/// Observer that updates temporal states on timer tick
|
||||||
|
pub fn on_timer_tick(
|
||||||
|
_: On<TickEvent>,
|
||||||
|
player_query: Query<(
|
||||||
|
Option<&mut states::Attacking>,
|
||||||
|
Option<&mut states::Awaiting>,
|
||||||
|
), With<Player>>
|
||||||
|
) {
|
||||||
|
for (maybe_attacking, maybe_awaiting) in player_query {
|
||||||
|
if let Some(mut attacking) = maybe_attacking {
|
||||||
|
info!("attack tick");
|
||||||
|
attacking.beats_remaining -= 1;
|
||||||
|
} else if let Some(mut awaiting) = maybe_awaiting {
|
||||||
|
info!("await tick");
|
||||||
|
awaiting.beats_remaining -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
src/player/triggers.rs
Normal file
41
src/player/triggers.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
//! Player state machine triggers
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use super::states::*;
|
||||||
|
|
||||||
|
/// 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>> {
|
||||||
|
(move |In(player_id): In<Entity>, query: Query<&InputState, With<Player>>| {
|
||||||
|
let Ok(input) = query.get(player_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
for action in actions.iter() {
|
||||||
|
if input.just_pressed(action) {
|
||||||
|
return Some(*action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).into_trigger()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition from [Free] to [Choosing]
|
||||||
|
pub fn from_free_to_choosing(trans: Trans<Free, PlayerInput>) -> Choosing {
|
||||||
|
Choosing { log: trans.out }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if player has [NextAttack]
|
||||||
|
pub fn attack_queued(In(player_id): In<Entity>, query: Query<&NextAttack>) -> Option<Attacking> {
|
||||||
|
match query.get(player_id) {
|
||||||
|
Ok(next) => Some(next.0),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition from [Choosing] to [Attacking]
|
||||||
|
pub fn from_choosing_to_attacking(trans: Trans<Choosing, Attacking>) -> Attacking { trans.out }
|
||||||
|
|
||||||
|
/// Transition from [Awaiting] to [Choosing]
|
||||||
|
pub fn from_awaiting_to_choosing(trans: Trans<Awaiting, PlayerInput>) -> Choosing {
|
||||||
|
Choosing { log: trans.out }
|
||||||
|
}
|
||||||
83
src/player/weapon/knife.rs
Normal file
83
src/player/weapon/knife.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
//! Knife implementation module
|
||||||
|
|
||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
use crate::meters;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Light cutting weapon
|
||||||
|
#[derive(Component, Clone, PartialEq, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct Knife {
|
||||||
|
level_multipliers: Vec<f32>,
|
||||||
|
base_damage: HashMap<AttackType, f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Knife {
|
||||||
|
fn default() -> Self {
|
||||||
|
let level_multipliers = vec![ 0., 1., 1.1, 1.2 ];
|
||||||
|
let mut base_damage = HashMap::new();
|
||||||
|
base_damage.insert(AttackType::HorizontalSwing, 5.);
|
||||||
|
base_damage.insert(AttackType::VerticalSwing, 5.);
|
||||||
|
base_damage.insert(AttackType::RoundSwing, 7.);
|
||||||
|
base_damage.insert(AttackType::Stab, 10.);
|
||||||
|
base_damage.insert(AttackType::Lunge, 10.);
|
||||||
|
Self { level_multipliers, base_damage }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Weapon for Knife {
|
||||||
|
fn graph(&self) -> ActionGraph<AttackType,PlayerInput> {
|
||||||
|
use PlayerInput::HeavyAttack as Heavy;
|
||||||
|
use PlayerInput::LightAttack as Light;
|
||||||
|
use AttackType as P;
|
||||||
|
|
||||||
|
let paths = vec![
|
||||||
|
vec![(Heavy, P::Lunge), (Heavy, P::VerticalSwing), (Heavy, P::Stab)],
|
||||||
|
vec![(Heavy, P::Lunge), (Heavy, P::VerticalSwing), (Light, P::RoundSwing)],
|
||||||
|
vec![(Heavy, P::Lunge), (Light, P::HorizontalSwing), (Light, P::HorizontalSwing)],
|
||||||
|
vec![(Light, P::HorizontalSwing), (Light, P::HorizontalSwing), (Heavy, P::Stab)],
|
||||||
|
vec![(Light, P::HorizontalSwing), (Light, P::HorizontalSwing), (Light, P::VerticalSwing)],
|
||||||
|
];
|
||||||
|
|
||||||
|
ActionGraph::from_paths(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attack_bundle(&self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea> {
|
||||||
|
let Some(action_type) = attack.action else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(damage) = self.base_damage.get(&action_type) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let damage = damage * self.level_multipliers[attack.level as usize];
|
||||||
|
|
||||||
|
let (duration, half_area) = match action_type {
|
||||||
|
AttackType::HorizontalSwing => {(
|
||||||
|
Duration::from_secs(1),
|
||||||
|
vec2(meters(0.5), meters(0.2)),
|
||||||
|
)},
|
||||||
|
AttackType::VerticalSwing => {(
|
||||||
|
Duration::from_millis(1_500),
|
||||||
|
vec2(meters(0.5), meters(1.)),
|
||||||
|
)},
|
||||||
|
AttackType::RoundSwing => {(
|
||||||
|
Duration::from_secs(1),
|
||||||
|
vec2(meters(0.5), meters(0.2)),
|
||||||
|
)},
|
||||||
|
AttackType::Stab => {(
|
||||||
|
Duration::from_secs(1),
|
||||||
|
vec2(meters(0.5), meters(0.1)),
|
||||||
|
)},
|
||||||
|
AttackType::Lunge => {(
|
||||||
|
Duration::from_millis(1_500),
|
||||||
|
vec2(meters(0.7), meters(0.1)),
|
||||||
|
)},
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(AttackArea::new(accuracy, damage, duration, half_area))
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/player/weapon/mod.rs
Normal file
34
src/player/weapon/mod.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
//! Equippable weapons module
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{combat::attack::AttackArea, graph::action::{ActionGraph, ActionNode}, input::PlayerInput};
|
||||||
|
|
||||||
|
pub mod knife;
|
||||||
|
|
||||||
|
/// Contains every possible attack type
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Reflect)]
|
||||||
|
#[reflect(Clone, PartialEq, Debug, Hash)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum AttackType {
|
||||||
|
/// Low damage attack, creates small attack area
|
||||||
|
HorizontalSwing,
|
||||||
|
/// Low damage attack, creates large attack area
|
||||||
|
VerticalSwing,
|
||||||
|
/// Medium damage attack, creates small attack area
|
||||||
|
RoundSwing,
|
||||||
|
/// High damage attack, creates very small attack area
|
||||||
|
Stab,
|
||||||
|
/// Like stab, but user dashes towards
|
||||||
|
Lunge,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait to obtain [ActionGraph] for given weapon
|
||||||
|
#[bevy_trait_query::queryable]
|
||||||
|
pub trait Weapon {
|
||||||
|
/// Returns [ActionGraph] for this weapon
|
||||||
|
fn graph(self: &Self) -> ActionGraph<AttackType, PlayerInput>;
|
||||||
|
|
||||||
|
/// Returns [AttackArea] for given attack
|
||||||
|
fn attack_bundle(self: &Self, attack: ActionNode<AttackType>, accuracy: f32) -> Option<AttackArea>;
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
use bevy_trait_query::RegisterExt;
|
||||||
use leafwing_input_manager::prelude::*;
|
use leafwing_input_manager::prelude::*;
|
||||||
use seldom_state::prelude::*;
|
use seldom_state::prelude::*;
|
||||||
|
|
||||||
use crate::*;
|
use crate::{*, player::weapon::*};
|
||||||
|
|
||||||
/// Plugin that connects everything needed for this prototype
|
/// Plugin that connects everything needed for this prototype
|
||||||
pub struct GamePlugin;
|
pub struct GamePlugin;
|
||||||
|
|
@ -26,6 +27,7 @@ impl Plugin for GamePlugin {
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
player::systems::handle_input,
|
player::systems::handle_input,
|
||||||
timer::update_bpm_timers,
|
timer::update_bpm_timers,
|
||||||
));
|
))
|
||||||
|
.register_component_as::<dyn Weapon, knife::Knife>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
//! Commonly used entity states
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
/// Entity is not doing anything special
|
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
|
||||||
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
|
||||||
pub struct Free;
|
|
||||||
|
|
||||||
/// Entity is acting and cannot do something else
|
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
|
||||||
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
|
||||||
pub struct Acting {
|
|
||||||
/// How much time left before action ends
|
|
||||||
pub beats_remaining: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Entity is awaiting a specific set of actions
|
|
||||||
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
|
||||||
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
|
||||||
pub struct ActionWindow {
|
|
||||||
/// How much time left before window ends
|
|
||||||
pub beats_remaining: u32,
|
|
||||||
}
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl BpmTimer {
|
||||||
/// Updates internal timer and returns true if it just ticked
|
/// Updates internal timer and returns true if it just ticked
|
||||||
pub fn tick(&mut self, dt: Duration) -> bool {
|
pub fn tick(&mut self, dt: Duration) -> bool {
|
||||||
let ticked = self.timer.tick(dt).just_finished();
|
let ticked = self.timer.tick(dt).just_finished();
|
||||||
if ticked { self.elapsed += 1.; }
|
if ticked { self.elapsed += self.timer.times_finished_this_tick() as f32; }
|
||||||
ticked
|
ticked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue