de_controller 0.0.1

Handling of user input in Digital Extinction.
Documentation
use bevy::{ecs::system::SystemParam, prelude::*, window::Windows};
use de_core::{stages::GameStage, state::GameState};
use de_index::SpatialQuery;
use de_terrain::TerrainCollider;
use glam::{Vec2, Vec3};
use iyes_loopless::prelude::*;
use parry3d::query::Ray;

use crate::Labels;

pub(crate) struct PointerPlugin;

impl Plugin for PointerPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<Pointer>().add_system_to_stage(
            GameStage::Input,
            mouse_move_handler
                .run_in_state(GameState::Playing)
                .label(Labels::PreInputUpdate),
        );
    }
}

#[derive(Default)]
pub(crate) struct Pointer {
    entity: Option<Entity>,
    terrain: Option<Vec3>,
}

impl Pointer {
    /// Pointed to entity or None if mouse is not over any entity.
    pub(crate) fn entity(&self) -> Option<Entity> {
        self.entity
    }

    /// Pointed to 3D position on the surface of the terrain. This can be below
    /// (occluded) another entity. It is None if the mouse is not over terrain
    /// at all.
    pub(crate) fn terrain_point(&self) -> Option<Vec3> {
        self.terrain
    }

    fn set_entity(&mut self, entity: Option<Entity>) {
        self.entity = entity;
    }

    fn set_terrain_point(&mut self, point: Option<Vec3>) {
        self.terrain = point;
    }
}

#[derive(SystemParam)]
struct MouseInWorld<'w, 's> {
    windows: Res<'w, Windows>,
    cameras: Query<'w, 's, (&'static Transform, &'static Camera), With<Camera3d>>,
}

impl<'w, 's> MouseInWorld<'w, 's> {
    fn mouse_ray(&self) -> Option<Ray> {
        let window = self.windows.get_primary().unwrap();

        // Normalized to values between -1.0 to 1.0 with (0.0, 0.0) in the
        // middle of the screen.
        let cursor_position = match window.cursor_position() {
            Some(position) => {
                let screen_size = Vec2::new(window.width(), window.height());
                (position / screen_size) * 2.0 - Vec2::ONE
            }
            None => return None,
        };

        let (camera_transform, camera) = self.cameras.single();
        let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();
        let ray_origin = ndc_to_world.project_point3(cursor_position.extend(1.));
        let ray_direction = ray_origin - camera_transform.translation;
        Some(Ray::new(ray_origin.into(), ray_direction.into()))
    }
}

fn mouse_move_handler(
    mut resource: ResMut<Pointer>,
    mouse: MouseInWorld,
    entities: SpatialQuery<()>,
    terrain: TerrainCollider,
) {
    let ray = mouse.mouse_ray();

    let entity = ray
        .as_ref()
        .and_then(|ray| entities.cast_ray(ray, f32::INFINITY, None))
        .map(|intersection| intersection.entity());
    resource.set_entity(entity);

    let terrain_point = ray
        .and_then(|ray| terrain.cast_ray(&ray, f32::INFINITY))
        .map(|intersection| ray.unwrap().point_at(intersection.toi).into());
    resource.set_terrain_point(terrain_point);
}