mittens-engine 0.7.0

A Vulkan and OpenXR scene engine with ECS, reactive signals, and Meow Meow scripting
use crate::engine::ecs::ComponentId;
use crate::engine::ecs::component::{Component, ComponentRef};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollisionResponseMode {
    /// Kinematic "slide" resolution: push the body out of static overlaps using AABB penetration.
    Slide,

    /// Kinematic "push" behavior: accumulate velocity away from overlapping non-static colliders,
    /// and integrate it every tick (still resolves against static overlaps).
    Push,
}

/// Opt-in kinematic collision response behavior for a collider.
///
/// Preferred topology is nesting under the collider:
///
/// `TransformComponent -> CollisionComponent -> CollisionResponseComponent`
#[derive(Debug, Clone)]
pub struct CollisionResponseComponent {
    pub enabled: bool,
    pub mode: CollisionResponseMode,

    /// Max number of correction iterations per tick (helps resolve corner cases).
    pub max_iterations: u32,

    /// Small extra push-out added on separation to avoid jittering on exact contact.
    pub push_out_epsilon: f32,

    /// Strength of velocity acceleration applied from overlapping non-static colliders.
    pub push_strength: f32,

    /// Optional friction-like damping applied to velocity each second.
    /// Off by default.
    /// Effective multiplier per tick is `max(0, 1 - friction * dt)`.
    pub friction: f32,

    /// Optional additional damping applied to the Y velocity component only when resolving
    /// a vertical (Y-axis) static overlap.
    /// Off by default.
    /// Effective multiplier per contact tick is `max(0, 1 - friction_y * dt)`.
    pub friction_y: f32,

    /// Clamp on velocity magnitude (world units / sec).
    pub max_speed: f32,

    /// Runtime-only velocity accumulator (not serialized).
    pub velocity: [f32; 3],

    /// Runtime-only cached gravity coefficient from the nearest enabled `GravityComponent`
    /// ancestor.
    ///
    /// This is set by `CollisionResponseSystem` when the component is registered.
    pub gravity_coefficient: f32,

    /// Authored movement destination, resolved again on every response frame.
    pub movement_target_source: Option<ComponentRef>,

    /// Runtime-only movement destination used by generated collider topologies.
    pub movement_target_id: Option<ComponentId>,

    pub(crate) movement_target_required: bool,

    component: Option<ComponentId>,
}

impl CollisionResponseComponent {
    pub fn new(mode: CollisionResponseMode) -> Self {
        Self {
            enabled: true,
            mode,
            max_iterations: 6,
            push_out_epsilon: 0.001,
            push_strength: 4.0,
            friction: 0.0,
            friction_y: 0.0,
            max_speed: 6.0,
            velocity: [0.0, 0.0, 0.0],
            gravity_coefficient: 0.0,
            movement_target_source: None,
            movement_target_id: None,
            movement_target_required: false,
            component: None,
        }
    }

    pub fn slide() -> Self {
        let mut c = Self::new(CollisionResponseMode::Slide);
        c.push_strength = 0.0;
        c.friction = 0.0;
        c.max_speed = 0.0;
        c
    }

    pub fn push() -> Self {
        Self::new(CollisionResponseMode::Push)
    }

    pub fn with_push_strength(mut self, push_strength: f32) -> Self {
        self.push_strength = push_strength;
        self
    }

    pub fn with_friction(mut self, friction: f32) -> Self {
        self.friction = friction.max(0.0);
        self
    }

    pub fn with_friction_y(mut self, friction_y: f32) -> Self {
        self.friction_y = friction_y.max(0.0);
        self
    }

    pub fn movement_target(mut self, target: ComponentRef) -> Self {
        self.movement_target_source = Some(target);
        self
    }

    pub(crate) fn with_runtime_movement_target(mut self, target: Option<ComponentId>) -> Self {
        self.movement_target_id = target;
        self.movement_target_required = true;
        self
    }
}

impl Default for CollisionResponseComponent {
    fn default() -> Self {
        Self::slide()
    }
}

impl Component for CollisionResponseComponent {
    fn name(&self) -> &'static str {
        "collision_response"
    }

    fn set_id(&mut self, component: ComponentId) {
        self.component = Some(component);
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }

    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
        emit.push_intent_now(
            component,
            crate::engine::ecs::IntentValue::RegisterCollisionResponse {
                component_id: component,
            },
        );
    }

    fn cleanup(
        &mut self,
        emit: &mut dyn crate::engine::ecs::SignalEmitter,
        component: ComponentId,
    ) {
        emit.push_intent_now(
            component,
            crate::engine::ecs::IntentValue::RemoveCollisionResponse {
                component_id: component,
            },
        );
    }

    fn to_mms_ast(
        &self,
        _world: &crate::engine::ecs::World,
    ) -> crate::scripting::ast::ComponentExpression {
        use crate::engine::ecs::component::ce_helpers::*;
        let ctor = match self.mode {
            CollisionResponseMode::Slide => "slide",
            CollisionResponseMode::Push => "push",
        };
        let mut ce = ce_call("CollisionResponse", ctor, vec![])
            .with_call("enabled", vec![b(self.enabled)])
            .with_call("max_iterations", vec![num(self.max_iterations as f64)])
            .with_call("push_out_epsilon", vec![num(self.push_out_epsilon as f64)])
            .with_call("push_strength", vec![num(self.push_strength as f64)])
            .with_call("friction", vec![num(self.friction as f64)])
            .with_call("friction_y", vec![num(self.friction_y as f64)])
            .with_call("max_speed", vec![num(self.max_speed as f64)]);
        if let Some(target) = &self.movement_target_source {
            let expr = match target {
                ComponentRef::Guid(guid) => {
                    crate::scripting::ast::Expression::String(format!("@uuid:{guid}"))
                }
                ComponentRef::Query(query) => {
                    crate::scripting::ast::Expression::String(query.clone())
                }
            };
            ce = ce.with_call("movement_target", vec![expr]);
        }
        ce
    }
}