bevy-ichun 0.5.0

A simple kinematic character controller for avian3d
Documentation
//! # Character Movement Systems
//!
//! This module provides systems and components that handle character movement based on input events.
//! It translates event data into velocity changes, handles jumping, running, and camera rotation.
//!
//! The movement systems work by responding to events dispatched by the input systems (whether your own or the ichun input module),
//! applying appropriate velocity changes to the character.
//!
//! ## Features
//!
//! * Smooth ground movement with acceleration-based controls
//! * Configurable running mode with increased speed
//! * Jumping mechanics with customizable impulse
//! * Floating / gliding mechanics with reduced gravity
//! * Controlled air movement with reduced control
//! * Camera rotation from mouse input
//!
//! ## Components
//!
//! * [`KccMovementConfig`]: Configures movement parameters like speed, jumping strength, etc.
//!
//! ## Events
//!
//! * [`IchunMoveEvent`]: Triggered when movement input is detected
//! * [`IchunRunEvent`]: Triggered when run input is detected
//! * [`IchunJumpEvent`]: Triggered when jump input is detected
//! * [`IchunFloatEvent`]: Triggered when float/glide input is detected
//! * [`IchunRotateEvent`]: Triggered when rotation input is detected
use avian3d::math::{Scalar, Vector, Vector2};
use bevy::prelude::*;

use crate::{
    actions::{
        KccActions, gravity_action::GravityAction, jump_action::JumpAction,
        move_action::MoveAction, rotate_action::RotateAction,
    },
    kcc::Kcc,
    system_sets::IchunSystemSet,
};

/// Configuration component for character movement capabilities
///
/// This component stores the parameters that control how a character moves,
/// including movement speed, running speed, jumping strength, camera sensitivity,
/// and air control parameters.
///
/// It requires the [`Kcc`] component to be present on the same entity.
///
/// # Example
///
/// ```rust
/// use ichun::kcc::Kcc;
/// use bevy_ichun::movement::KccMovementConfig;
///
/// // Create a custom movement configuration
/// let movement_config = KccMovementConfig {
///     movement_acceleration: 25.0,
///     running_acceleration: 50.0,
///     jump_impulse: 12.0,
///     camera_sensitivity: Vector2::new(0.002, 0.001),
///     ..Default::default()
/// };
/// ```
#[derive(Component)]
#[require(Kcc)]
pub struct KccMovementEventsConfig {
    /// The acceleration used for character movement.
    /// Higher values make the character accelerate faster.
    pub movement_acceleration: Scalar,
    /// The acceleration used for running.
    /// Higher values make the character run faster.
    pub running_acceleration: Scalar,
    /// How much control the character has in the air (0.0 - 1.0).
    /// Lower values give less control, higher values give more control.
    pub air_control_factor: Scalar,
    /// How quickly air control takes effect.
    /// Higher values make air movement more responsive.
    pub air_acceleration_factor: Scalar,
    /// Max speed in air relative to ground speed.
    /// Values above 1.0 allow faster movement in air than on ground.
    pub air_max_speed_multiplier: Scalar,
    /// The sensitivity used for character rotation (camera movement).
    /// The X component affects horizontal rotation, Y affects vertical.
    pub camera_sensitivity: Vector2,
    /// The strength of a jump impulse.
    /// Higher values make the character jump higher.
    pub jump_impulse: Scalar,
    /// Whether the character is currently running.
    pub is_running: bool,
    /// Whether the character is currently floating / gliding.
    pub is_floating: bool,
    /// The gravity being applied when KCC is floating.
    /// This is typically less than normal gravity to slow falling.
    pub floating_gravity: Vector,
}

impl Default for KccMovementEventsConfig {
    fn default() -> Self {
        Self {
            movement_acceleration: 50.0,
            running_acceleration: 90.0,
            air_control_factor: 0.5,
            air_acceleration_factor: 1.5,
            air_max_speed_multiplier: 1.1,
            camera_sensitivity: Vector2::new(0.003, 0.002),
            jump_impulse: 7.0,
            is_running: false,
            is_floating: false,
            floating_gravity: Vector::NEG_Y * 4.0,
        }
    }
}

pub struct IchunMovementEventsPlugin;

impl Plugin for IchunMovementEventsPlugin {
    fn build(&self, app: &mut bevy::app::App) {
        app.add_event::<IchunRotateEvent>()
            .add_event::<IchunMoveEvent>()
            .add_event::<IchunRunEvent>()
            .add_event::<IchunJumpEvent>()
            .add_event::<IchunFloatEvent>()
            .add_systems(
                Update,
                (
                    handle_rotate_events_sys,
                    handle_move_events_sys,
                    handle_run_events_sys,
                    handle_jump_events_sys,
                    handle_float_events_sys,
                    cap_gravity_sys,
                )
                    .chain()
                    .in_set(IchunSystemSet::MovementEventsSet),
            );
    }
}

/// Event triggered when character movement input is detected
///
/// This event is dispatched by input systems when the player presses
/// movement keys or uses an analog stick. The direction is a normalized
/// 2D vector representing the input direction.
///
/// # Fields
///
/// * `entity`: The entity (character) that should move
/// * `direction`: A normalized 2D vector representing movement direction
#[derive(Event)]
pub struct IchunMoveEvent {
    pub entity: Entity,
    pub direction: Vector2,
}

/// Event triggered when character run input is detected
///
/// This event is dispatched when the player activates or deactivates
/// the run mode. It can toggle the running state or set it explicitly.
///
/// # Fields
///
/// * `entity`: The entity (character) that should run
/// * `is_running`:
///   - `Some(true)`: Enable running
///   - `Some(false)`: Disable running
///   - `None`: Toggle running state
#[derive(Event)]
pub struct IchunRunEvent {
    pub entity: Entity,
    pub is_running: Option<bool>,
}

/// Event triggered when character rotation input is detected
///
/// This event is dispatched when the player moves the mouse
/// to rotate the character or camera.
///
/// # Fields
///
/// * `entity`: The entity (character) that should rotate
/// * `rotation`: A 2D vector representing the rotation amount
///   (typically derived from mouse movement)
#[derive(Event)]
pub struct IchunRotateEvent {
    pub entity: Entity,
    pub rotation: Vector2,
}

/// Event triggered when character jump input is detected
///
/// This event is dispatched when the player presses the jump button.
/// The jump will only occur if the character is grounded.
///, mut actions
/// # Fields
///
/// * `entity`: The entity (character) that should jump
#[derive(Event)]
pub struct IchunJumpEvent {
    pub entity: Entity,
}

/// Event triggered when character float/glide input is detected
///
/// This event is dispatched when the player activates or deactivates
/// the floating / gliding mode. It can toggle the floating state or set it explicitly.
///
/// # Fields
///
/// * `entity`: The entity (character) that should float
/// * `is_floating`:
///   - `Some(true)`: Enable floating
///   - `Some(false)`: Disable floating
///   - `None`: Toggle floating state
#[derive(Event)]
pub struct IchunFloatEvent {
    pub entity: Entity,
    pub is_floating: Option<bool>,
}

/// Responds to [`IchunMoveEvent`] events and moves character controllers accordingly.
fn handle_move_events_sys(
    mut movement_event_reader: EventReader<IchunMoveEvent>,
    mut controllers_qry: Query<(Entity, &KccMovementEventsConfig, &mut KccActions)>,
) {
    for event in movement_event_reader.read() {
        let Some((_, config, mut actions)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        // Convert 2D input direction to 3D desired velocity
        let desired_velocity = Vec3::new(event.direction.x, 0.0, event.direction.y);

        // Determine acceleration based on running state
        let acceleration = if config.is_running {
            config.running_acceleration
        } else {
            config.movement_acceleration
        };

        actions.move_action = Some(
            MoveAction::new(desired_velocity)
                .with_movement_acceleration(acceleration)
                .with_air_control(config.air_control_factor)
                .with_air_acceleration_factor(config.air_acceleration_factor)
                .with_air_max_speed_multiplier(config.air_max_speed_multiplier)
                .with_account_rotation(true),
        );
    }
}

/// Responds to [`IchunRotateEvent`] events and moves character controllers accordingly.
fn handle_rotate_events_sys(
    mut movement_event_reader: EventReader<IchunRotateEvent>,
    mut controllers_qry: Query<(Entity, &KccMovementEventsConfig, &mut KccActions)>,
) {
    for event in movement_event_reader.read() {
        let Some((_, config, mut actions)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        // Convert 2D rotation to 3D (keeping Y as pitch, X as yaw)
        let rotation = Vec3::new(event.rotation.x * config.camera_sensitivity.x, 0.0, 0.0);

        actions.rotation_action = Some(RotateAction::new(rotation));
    }
}

/// Responds to [`IchunJumpEvent`] events and moves character controllers accordingly.
fn handle_jump_events_sys(
    mut movement_event_reader: EventReader<IchunJumpEvent>,
    mut controllers_qry: Query<(Entity, &KccMovementEventsConfig, &mut KccActions)>,
) {
    for event in movement_event_reader.read() {
        let Some((_, config, mut actions)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        actions.jump_action = Some(JumpAction::new(config.jump_impulse).with_allow_in_air(true)); // Default to ground-only jumping
    }
}

/// Responds to [`IchunRunEvent`] events and changes run state accordingly.
fn handle_run_events_sys(
    mut movement_event_reader: EventReader<IchunRunEvent>,
    mut controllers_qry: Query<(Entity, &mut KccMovementEventsConfig)>,
) {
    for event in movement_event_reader.read() {
        let Some((_, mut config)) = controllers_qry.iter_mut().find(|c| c.0 == event.entity) else {
            continue;
        };

        // If is_running is Some, set to that value
        // If is_running is None, toggle the current state
        config.is_running = match event.is_running {
            Some(running) => running,
            None => !config.is_running,
        };
    }
}

/// Responds to [`IchunFloatEvent`] events and changes float state accordingly.
fn handle_float_events_sys(
    mut movement_event_reader: EventReader<IchunFloatEvent>,
    mut controllers_qry: Query<(Entity, &mut KccMovementEventsConfig)>,
) {
    for event in movement_event_reader.read() {
        let Some((_, mut config)) = controllers_qry.iter_mut().find(|c| c.0 == event.entity) else {
            continue;
        };

        // If is_floating is Some, set to that value
        // If is_floating is None, toggle the current state
        config.is_floating = match event.is_floating {
            Some(floating) => floating,
            None => !config.is_floating,
        };
    }
}

/// System which caps the gravity to the floating gravity if Kcc is floating
fn cap_gravity_sys(mut controllers_qry: Query<(&KccMovementEventsConfig, &mut KccActions)>) {
    for (movement, mut actions) in controllers_qry.iter_mut() {
        if movement.is_floating {
            if movement.is_floating {
                actions.gravity_action = Some(GravityAction::new(movement.floating_gravity));
            } else {
                actions.gravity_action = None;
            }
        }
    }
}