348 lines
11 KiB
Rust
348 lines
11 KiB
Rust
//! Input handling using engine GameActions
|
|
//!
|
|
//! Processes GameActions (from InputController) and applies them to game entities.
|
|
|
|
use bevy::prelude::*;
|
|
use libmarathon::{
|
|
engine::GameAction,
|
|
platform::input::InputController,
|
|
networking::{
|
|
EntityLockRegistry, LocalSelection, NetworkedEntity,
|
|
NodeVectorClock,
|
|
},
|
|
};
|
|
|
|
use crate::cube::CubeMarker;
|
|
|
|
use super::event_buffer::InputEventBuffer;
|
|
|
|
pub struct InputHandlerPlugin;
|
|
|
|
impl Plugin for InputHandlerPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<InputControllerResource>()
|
|
// handle_game_actions updates selection - must run before release_locks_on_deselection_system
|
|
.add_systems(Update, handle_game_actions.before(libmarathon::networking::release_locks_on_deselection_system))
|
|
.add_systems(PostUpdate, update_lock_visuals);
|
|
}
|
|
}
|
|
|
|
/// Resource wrapping the InputController
|
|
#[derive(Resource)]
|
|
struct InputControllerResource {
|
|
controller: InputController,
|
|
}
|
|
|
|
impl Default for InputControllerResource {
|
|
fn default() -> Self {
|
|
Self {
|
|
controller: InputController::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert glam::Vec2 to Bevy's Vec2
|
|
///
|
|
/// They're the same type, just construct a new one.
|
|
#[inline]
|
|
fn to_bevy_vec2(v: glam::Vec2) -> bevy::math::Vec2 {
|
|
bevy::math::Vec2::new(v.x, v.y)
|
|
}
|
|
|
|
/// Process GameActions and apply to entities
|
|
fn handle_game_actions(
|
|
input_buffer: Res<InputEventBuffer>,
|
|
mut controller_res: ResMut<InputControllerResource>,
|
|
lock_registry: Res<EntityLockRegistry>,
|
|
node_clock: Res<NodeVectorClock>,
|
|
mut selection: ResMut<LocalSelection>,
|
|
mut cube_query: Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
camera_query: Query<(&Camera, &GlobalTransform)>,
|
|
window_query: Query<&Window>,
|
|
) {
|
|
let node_id = node_clock.node_id;
|
|
|
|
// Process all input events through the controller to get game actions
|
|
let mut all_actions = Vec::new();
|
|
for event in input_buffer.events.iter() {
|
|
let actions = controller_res.controller.process_event(event);
|
|
all_actions.extend(actions);
|
|
}
|
|
|
|
// Apply game actions to entities
|
|
for action in all_actions {
|
|
match action {
|
|
GameAction::SelectEntity { position } => {
|
|
// Do raycasting to find which entity (if any) was clicked
|
|
let entity_id = raycast_entity(
|
|
position,
|
|
&cube_query,
|
|
&camera_query,
|
|
&window_query,
|
|
);
|
|
|
|
// Update selection
|
|
// The release_locks_on_deselection_system will automatically handle lock changes
|
|
selection.clear();
|
|
if let Some(id) = entity_id {
|
|
selection.insert(id);
|
|
info!("Selected entity {}", id);
|
|
} else {
|
|
info!("Deselected all entities");
|
|
}
|
|
}
|
|
|
|
GameAction::MoveEntity { delta } => {
|
|
apply_move_entity(delta, &lock_registry, node_id, &mut cube_query);
|
|
}
|
|
|
|
GameAction::RotateEntity { delta } => {
|
|
apply_rotate_entity(delta, &lock_registry, node_id, &mut cube_query);
|
|
}
|
|
|
|
GameAction::MoveEntityDepth { delta } => {
|
|
apply_move_depth(delta, &lock_registry, node_id, &mut cube_query);
|
|
}
|
|
|
|
GameAction::ResetEntity => {
|
|
apply_reset_entity(&lock_registry, node_id, &mut cube_query);
|
|
}
|
|
|
|
_ => {
|
|
// Other actions not yet implemented
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Raycast to find which entity was clicked
|
|
///
|
|
/// Returns the network ID of the closest entity hit by the ray, or None if nothing was hit.
|
|
fn raycast_entity(
|
|
position: glam::Vec2,
|
|
cube_query: &Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
camera_query: &Query<(&Camera, &GlobalTransform)>,
|
|
window_query: &Query<&Window>,
|
|
) -> Option<uuid::Uuid> {
|
|
// Get the camera and window
|
|
let Ok((camera, camera_transform)) = camera_query.single() else {
|
|
return None;
|
|
};
|
|
let Ok(window) = window_query.single() else {
|
|
return None;
|
|
};
|
|
|
|
// Convert screen position to world ray
|
|
let Some(ray) = screen_to_world_ray(position, camera, camera_transform, window) else {
|
|
return None;
|
|
};
|
|
|
|
// Find the closest cube hit by the ray
|
|
let mut closest_hit: Option<(uuid::Uuid, f32)> = None;
|
|
|
|
for (networked, transform) in cube_query.iter() {
|
|
// Test ray against cube AABB (1x1x1 cube)
|
|
if let Some(distance) = ray_aabb_intersection(
|
|
ray.origin,
|
|
ray.direction,
|
|
transform.translation,
|
|
Vec3::splat(0.5), // Half extents for 1x1x1 cube
|
|
) {
|
|
if closest_hit.map_or(true, |(_, d)| distance < d) {
|
|
closest_hit = Some((networked.network_id, distance));
|
|
}
|
|
}
|
|
}
|
|
|
|
closest_hit.map(|(entity_id, _)| entity_id)
|
|
}
|
|
|
|
/// Apply MoveEntity action to locked cubes
|
|
fn apply_move_entity(
|
|
delta: glam::Vec2,
|
|
lock_registry: &EntityLockRegistry,
|
|
node_id: uuid::Uuid,
|
|
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
) {
|
|
let bevy_delta = to_bevy_vec2(delta);
|
|
let sensitivity = 0.01; // Scale factor
|
|
|
|
for (networked, mut transform) in cube_query.iter_mut() {
|
|
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
transform.translation.x += bevy_delta.x * sensitivity;
|
|
transform.translation.y -= bevy_delta.y * sensitivity; // Invert Y for screen coords
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply RotateEntity action to locked cubes
|
|
fn apply_rotate_entity(
|
|
delta: glam::Vec2,
|
|
lock_registry: &EntityLockRegistry,
|
|
node_id: uuid::Uuid,
|
|
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
) {
|
|
let bevy_delta = to_bevy_vec2(delta);
|
|
let sensitivity = 0.01;
|
|
|
|
for (networked, mut transform) in cube_query.iter_mut() {
|
|
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
let rotation_x = Quat::from_rotation_y(bevy_delta.x * sensitivity);
|
|
let rotation_y = Quat::from_rotation_x(-bevy_delta.y * sensitivity);
|
|
transform.rotation = rotation_x * transform.rotation * rotation_y;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply MoveEntityDepth action to locked cubes
|
|
fn apply_move_depth(
|
|
delta: f32,
|
|
lock_registry: &EntityLockRegistry,
|
|
node_id: uuid::Uuid,
|
|
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
) {
|
|
let sensitivity = 0.1;
|
|
|
|
for (networked, mut transform) in cube_query.iter_mut() {
|
|
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
transform.translation.z += delta * sensitivity;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply ResetEntity action to locked cubes
|
|
fn apply_reset_entity(
|
|
lock_registry: &EntityLockRegistry,
|
|
node_id: uuid::Uuid,
|
|
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
) {
|
|
for (networked, mut transform) in cube_query.iter_mut() {
|
|
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
transform.translation = Vec3::ZERO;
|
|
transform.rotation = Quat::IDENTITY;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A 3D ray for raycasting
|
|
struct Ray {
|
|
origin: Vec3,
|
|
direction: Vec3,
|
|
}
|
|
|
|
/// Convert screen coordinates to a world-space ray from the camera
|
|
fn screen_to_world_ray(
|
|
screen_pos: glam::Vec2,
|
|
camera: &Camera,
|
|
camera_transform: &GlobalTransform,
|
|
_window: &Window,
|
|
) -> Option<Ray> {
|
|
// Convert screen position to viewport position (0..1 range)
|
|
let viewport_pos = Vec2::new(screen_pos.x, screen_pos.y);
|
|
|
|
// Use Bevy's viewport_to_world method
|
|
let ray_bevy = camera.viewport_to_world(camera_transform, viewport_pos).ok()?;
|
|
|
|
Some(Ray {
|
|
origin: ray_bevy.origin,
|
|
direction: *ray_bevy.direction,
|
|
})
|
|
}
|
|
|
|
/// Test ray-AABB (axis-aligned bounding box) intersection
|
|
///
|
|
/// Returns the distance along the ray if there's an intersection, None otherwise.
|
|
fn ray_aabb_intersection(
|
|
ray_origin: Vec3,
|
|
ray_direction: Vec3,
|
|
aabb_center: Vec3,
|
|
aabb_half_extents: Vec3,
|
|
) -> Option<f32> {
|
|
// Calculate AABB min and max
|
|
let aabb_min = aabb_center - aabb_half_extents;
|
|
let aabb_max = aabb_center + aabb_half_extents;
|
|
|
|
// Slab method for ray-AABB intersection
|
|
let mut tmin = f32::NEG_INFINITY;
|
|
let mut tmax = f32::INFINITY;
|
|
|
|
for i in 0..3 {
|
|
let origin_component = ray_origin[i];
|
|
let dir_component = ray_direction[i];
|
|
let min_component = aabb_min[i];
|
|
let max_component = aabb_max[i];
|
|
|
|
if dir_component.abs() < f32::EPSILON {
|
|
// Ray is parallel to slab, check if origin is within slab
|
|
if origin_component < min_component || origin_component > max_component {
|
|
return None;
|
|
}
|
|
} else {
|
|
// Compute intersection t values for near and far plane
|
|
let inv_dir = 1.0 / dir_component;
|
|
let mut t1 = (min_component - origin_component) * inv_dir;
|
|
let mut t2 = (max_component - origin_component) * inv_dir;
|
|
|
|
// Ensure t1 is the near intersection
|
|
if t1 > t2 {
|
|
std::mem::swap(&mut t1, &mut t2);
|
|
}
|
|
|
|
// Update tmin and tmax
|
|
tmin = tmin.max(t1);
|
|
tmax = tmax.min(t2);
|
|
|
|
// Check for intersection failure
|
|
if tmin > tmax {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If tmin is negative, the ray origin is inside the AABB
|
|
// Return tmax in that case, otherwise return tmin
|
|
if tmin < 0.0 {
|
|
if tmax < 0.0 {
|
|
return None; // AABB is behind the ray
|
|
}
|
|
Some(tmax)
|
|
} else {
|
|
Some(tmin)
|
|
}
|
|
}
|
|
|
|
/// System to update visual appearance based on lock state
|
|
///
|
|
/// Color scheme:
|
|
/// - Green: Locked by us (we can edit)
|
|
/// - Blue: Locked by someone else (they can edit, we can't)
|
|
/// - Pink: Not locked (nobody is editing)
|
|
fn update_lock_visuals(
|
|
lock_registry: Res<EntityLockRegistry>,
|
|
node_clock: Res<NodeVectorClock>,
|
|
mut cubes: Query<(&NetworkedEntity, &mut MeshMaterial3d<StandardMaterial>), With<CubeMarker>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
for (networked, material_handle) in cubes.iter_mut() {
|
|
let entity_id = networked.network_id;
|
|
|
|
// Determine color based on lock state
|
|
let node_id = node_clock.node_id;
|
|
let color = if lock_registry.is_locked_by(entity_id, node_id, node_id) {
|
|
// Locked by us - green
|
|
Color::srgb(0.3, 0.8, 0.3)
|
|
} else if lock_registry.is_locked(entity_id, node_id) {
|
|
// Locked by someone else - blue
|
|
Color::srgb(0.3, 0.5, 0.9)
|
|
} else {
|
|
// Not locked - default pink
|
|
Color::srgb(0.8, 0.3, 0.6)
|
|
};
|
|
|
|
// Update material color
|
|
if let Some(mat) = materials.get_mut(&material_handle.0) {
|
|
mat.base_color = color;
|
|
}
|
|
}
|
|
}
|