diff --git a/src/ai/jumper/mod.rs b/src/ai/jumper/mod.rs new file mode 100644 index 0000000..3918214 --- /dev/null +++ b/src/ai/jumper/mod.rs @@ -0,0 +1,106 @@ +//! Jumper enemy module + +use super::*; +use states::*; +use triggers::*; + +use crate::{GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, ai::jumper::states::Approaching, combat::Health, curve::BallisticCurve, meters, timer::WantsTickEvent}; + +pub mod states; +pub mod systems; +pub mod triggers; + +/// Applies impulse to entity using its direction to [AiTarget] +#[derive(Clone, Copy, PartialEq, Debug, Default, Reflect)] +#[reflect(Clone, PartialEq, Debug, Default)] +pub struct ApplyDirectedImpulse { + impulse: Vec2, + torque_impulse: f32, +} + +impl EntityCommand for ApplyDirectedImpulse { + fn apply(self, mut entity: EntityWorldMut) -> () { + let Some(entity_t) = entity.get::() else { + return; + }; + let Some(AiTarget(target_id)) = entity.get::() else { + return; + }; + let Some(target_t) = entity.world().get::(*target_id) else { + return; + }; + let direction = (target_t.translation.x - entity_t.translation.x).signum(); + let impulse = ExternalImpulse { + impulse: self.impulse.with_x(self.impulse.x * direction), + torque_impulse: self.torque_impulse, + }; + info!("Sent {impulse:#?}"); + entity.insert(impulse); + } +} + +/// Jumper enemy +#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)] +#[reflect(Component, Clone, PartialEq, Debug, Default)] +#[require(Health, StateMachine, Transform, WantsTickEvent)] +pub struct Jumper; + +impl Jumper { + /// Constructs [Jumper] bundle + pub fn bundle(position: Vec2, health: f32) -> impl Bundle { + ( + // Basic + Jumper, + Health::new(health), + + // World + Transform::from_xyz(position.x, position.y, 0.), + + // Collision + RigidBody::Dynamic, + LockedAxes::ROTATION_LOCKED, + Collider::cuboid(meters(0.5), meters(0.5)), + ActiveEvents::COLLISION_EVENTS, + ActiveCollisionTypes::all(), + CollisionGroups::new( + GROUP_ENEMY, + GROUP_STATIC | GROUP_INTERACTIVE | GROUP_FRIENDLY | GROUP_ATTACK, + ), + Velocity::zero(), + Friction { + coefficient: 0., + combine_rule: CoefficientCombineRule::Min, + }, + + // StateMachine + states::Idle::default(), + StateMachine::default() + // On enter Idle + .on_enter::(|e| { + e.insert(Velocity::zero()); + }) + // Idle -> Approaching + .trans::(target_in_range(Idle::MIN_DISTANCE), Approaching) + // Approaching -> Attacking + .trans::( + ticked + .ignore_and(target_in_range(Approaching::MIN_DISTANCE).not()) + .ignore_and(target_in_range(Approaching::MAX_DISTANCE)), + Attacking::new(), + ) + // On enter Attacking + .on_enter::(|e| { + e.insert(Velocity::zero()); + e.queue(ApplyDirectedImpulse { + impulse: vec2(12000., 9000.), + ..default() + }); + }) + // Approaching -> Idle + .trans::(target_in_range(Approaching::MAX_RANGE).not(), Idle) + // Attacking -> Approaching + .trans::(done(None), Approaching) + .set_trans_logging(true), + ) + } +} diff --git a/src/ai/jumper/states.rs b/src/ai/jumper/states.rs new file mode 100644 index 0000000..d675874 --- /dev/null +++ b/src/ai/jumper/states.rs @@ -0,0 +1,51 @@ +//! Jumper states + +use bevy::time::Stopwatch; + +use super::*; + +/// Entity awaits until [AiTarget] comes closer, then transits to [Approaching] +#[derive(Component, Clone, Copy, PartialEq, Debug, Default, Reflect)] +#[reflect(Component, Clone, PartialEq, Debug, Default)] +pub struct Idle; + +impl Idle { + /// Minimal distance required for next state in pixels + pub const MIN_DISTANCE: f32 = meters(4.); +} + +/// Approaches distance, required to jump, awaits for next [BpmTimer](crate::timer::BpmTimer) tick, then transits to [Attacking] +/// If target distances from entity very far, transits to [Idle] +#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect, Default)] +#[reflect(Component, Clone, PartialEq, Debug, Default)] +pub struct Approaching; + +impl Approaching { + /// Minimal distance in pixels, when entity should transit to [Attacking] + pub const MIN_DISTANCE: f32 = meters(2.); + /// Maximal distance in pixels, when entity should transit to [Attacking] + pub const MAX_DISTANCE: f32 = meters(3.); + /// Distance in pixels, when entity should transit to [Idle] + pub const MAX_RANGE: f32 = meters(6.); + /// Approaching speed, px/s + pub const SPEED: f32 = meters(0.5); +} + +/// Jumps at the [AiTarget] +/// If target was reached and jumper was not hit, it deals damage +/// Transits to [Approaching] when reaches the floor +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Clone, Debug)] +pub struct Attacking { + /// May deal damage to [AiTarget] + pub may_attack: bool, +} + +impl Attacking { + /// Constructs new [Attacking] state + pub fn new() -> Self { + Self { may_attack: true } + } + /// Damage applied to the [AiTarget] + pub const DAMAGE: f32 = 20.; +} diff --git a/src/ai/jumper/systems.rs b/src/ai/jumper/systems.rs new file mode 100644 index 0000000..49dd3e4 --- /dev/null +++ b/src/ai/jumper/systems.rs @@ -0,0 +1,78 @@ +//! Jumper systems + +use std::time::Duration; + +use crate::combat::attack::AttackArea; + +use super::*; + +/// Update in [Approaching] or [Attacking] state +pub fn update( + mut commands: Commands, + time: Res