mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-05 23:16:23 -06:00
351 lines
11 KiB
Rust
351 lines
11 KiB
Rust
//! Platform-agnostic input handling for the game
|
|
//!
|
|
//! This module provides input types and utilities that work across
|
|
//! all platforms (WASM, Tauri) without depending on Bevy's input system.
|
|
|
|
use bevy_ecs::prelude::Resource;
|
|
|
|
/// Mouse button identifier
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum MouseButton {
|
|
Left = 0,
|
|
Middle = 1,
|
|
Right = 2,
|
|
Back = 3,
|
|
Forward = 4,
|
|
}
|
|
|
|
impl MouseButton {
|
|
pub fn from_u8(button: u8) -> Option<Self> {
|
|
match button {
|
|
0 => Some(Self::Left),
|
|
1 => Some(Self::Middle),
|
|
2 => Some(Self::Right),
|
|
3 => Some(Self::Back),
|
|
4 => Some(Self::Forward),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Keyboard key codes (subset we actually use)
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum KeyCode {
|
|
KeyW,
|
|
KeyA,
|
|
KeyS,
|
|
KeyD,
|
|
KeyC,
|
|
Digit1,
|
|
Digit2,
|
|
Space,
|
|
Escape,
|
|
}
|
|
|
|
impl KeyCode {
|
|
pub fn from_string(key: &str) -> Option<Self> {
|
|
match key {
|
|
"KeyW" | "w" => Some(Self::KeyW),
|
|
"KeyA" | "a" => Some(Self::KeyA),
|
|
"KeyS" | "s" => Some(Self::KeyS),
|
|
"KeyD" | "d" => Some(Self::KeyD),
|
|
"KeyC" | "c" => Some(Self::KeyC),
|
|
"Digit1" | "1" => Some(Self::Digit1),
|
|
"Digit2" | "2" => Some(Self::Digit2),
|
|
"Space" | " " => Some(Self::Space),
|
|
"Escape" => Some(Self::Escape),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Button state (pressed or released)
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ButtonState {
|
|
Pressed,
|
|
Released,
|
|
}
|
|
|
|
/// World coordinates (in game units)
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct WorldPos {
|
|
pub x: f32,
|
|
pub y: f32,
|
|
}
|
|
|
|
/// Screen coordinates (in pixels)
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ScreenPos {
|
|
pub x: f32,
|
|
pub y: f32,
|
|
}
|
|
|
|
/// Tile coordinates on the map
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct TileCoord {
|
|
pub x: u32,
|
|
pub y: u32,
|
|
}
|
|
|
|
impl TileCoord {
|
|
/// Convert to linear tile index
|
|
pub fn to_index(&self, map_width: u32) -> usize {
|
|
(self.y * map_width + self.x) as usize
|
|
}
|
|
|
|
/// Create from linear tile index
|
|
pub fn from_index(index: usize, map_width: u32) -> Self {
|
|
Self { x: (index as u32) % map_width, y: (index as u32) / map_width }
|
|
}
|
|
}
|
|
|
|
/// Camera state for coordinate conversions
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct CameraState {
|
|
/// Camera position in world coordinates
|
|
pub x: f32,
|
|
pub y: f32,
|
|
/// Camera zoom level (1.0 = normal)
|
|
pub zoom: f32,
|
|
/// Viewport width in pixels
|
|
pub viewport_width: f32,
|
|
/// Viewport height in pixels
|
|
pub viewport_height: f32,
|
|
}
|
|
|
|
/// Input event from the frontend
|
|
#[derive(Debug, Clone)]
|
|
pub enum InputEvent {
|
|
MouseButton { button: MouseButton, state: ButtonState, world_pos: Option<WorldPos>, tile: Option<TileCoord> },
|
|
MouseMove { world_pos: WorldPos, screen_pos: ScreenPos, tile: Option<TileCoord> },
|
|
MouseWheel { delta_x: f32, delta_y: f32 },
|
|
KeyPress { key: KeyCode, state: ButtonState },
|
|
}
|
|
|
|
#[derive(Debug, Default, Resource)]
|
|
pub struct InputState {
|
|
// Mouse state
|
|
mouse_buttons: Vec<(MouseButton, ButtonState)>,
|
|
cursor_world_pos: Option<WorldPos>,
|
|
cursor_tile: Option<TileCoord>,
|
|
mouse_wheel_delta: (f32, f32),
|
|
|
|
// Keyboard state
|
|
keys_pressed: Vec<KeyCode>,
|
|
keys_just_pressed: Vec<KeyCode>,
|
|
keys_just_released: Vec<KeyCode>,
|
|
|
|
// Track if camera was interacted with (for click filtering)
|
|
camera_interaction: bool,
|
|
}
|
|
|
|
impl InputState {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Clear per-frame data (call at start of frame)
|
|
pub fn clear_frame_data(&mut self) {
|
|
self.mouse_buttons.clear();
|
|
self.keys_just_pressed.clear();
|
|
self.keys_just_released.clear();
|
|
self.mouse_wheel_delta = (0.0, 0.0);
|
|
self.camera_interaction = false;
|
|
}
|
|
|
|
/// Process an input event
|
|
pub fn handle_event(&mut self, event: InputEvent) {
|
|
match event {
|
|
InputEvent::MouseButton { button, state, world_pos, tile } => {
|
|
self.mouse_buttons.push((button, state));
|
|
if world_pos.is_some() {
|
|
self.cursor_world_pos = world_pos;
|
|
}
|
|
if tile.is_some() {
|
|
self.cursor_tile = tile;
|
|
}
|
|
}
|
|
InputEvent::MouseMove { world_pos, tile, .. } => {
|
|
self.cursor_world_pos = Some(world_pos);
|
|
self.cursor_tile = tile;
|
|
}
|
|
InputEvent::MouseWheel { delta_x, delta_y } => {
|
|
self.mouse_wheel_delta.0 += delta_x;
|
|
self.mouse_wheel_delta.1 += delta_y;
|
|
// Mouse wheel = camera interaction
|
|
if delta_x.abs() > 0.0 || delta_y.abs() > 0.0 {
|
|
self.camera_interaction = true;
|
|
}
|
|
}
|
|
InputEvent::KeyPress { key, state } => match state {
|
|
ButtonState::Pressed => {
|
|
if !self.keys_pressed.contains(&key) {
|
|
self.keys_pressed.push(key);
|
|
self.keys_just_pressed.push(key);
|
|
}
|
|
}
|
|
ButtonState::Released => {
|
|
self.keys_pressed.retain(|&k| k != key);
|
|
self.keys_just_released.push(key);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Check if a mouse button was just pressed this frame
|
|
pub fn mouse_just_pressed(&self, button: MouseButton) -> bool {
|
|
self.mouse_buttons.iter().any(|&(b, s)| b == button && s == ButtonState::Pressed)
|
|
}
|
|
|
|
/// Check if a mouse button was just released this frame
|
|
pub fn mouse_just_released(&self, button: MouseButton) -> bool {
|
|
self.mouse_buttons.iter().any(|&(b, s)| b == button && s == ButtonState::Released)
|
|
}
|
|
|
|
/// Check if a key is currently pressed
|
|
pub fn key_pressed(&self, key: KeyCode) -> bool {
|
|
self.keys_pressed.contains(&key)
|
|
}
|
|
|
|
/// Check if a key was just pressed this frame
|
|
pub fn key_just_pressed(&self, key: KeyCode) -> bool {
|
|
self.keys_just_pressed.contains(&key)
|
|
}
|
|
|
|
/// Check if a key was just released this frame
|
|
pub fn key_just_released(&self, key: KeyCode) -> bool {
|
|
self.keys_just_released.contains(&key)
|
|
}
|
|
|
|
/// Get current cursor position in world coordinates
|
|
pub fn cursor_world_pos(&self) -> Option<WorldPos> {
|
|
self.cursor_world_pos
|
|
}
|
|
|
|
/// Get current tile under cursor
|
|
pub fn cursor_tile(&self) -> Option<TileCoord> {
|
|
self.cursor_tile
|
|
}
|
|
|
|
/// Get mouse wheel delta for this frame
|
|
pub fn mouse_wheel_delta(&self) -> (f32, f32) {
|
|
self.mouse_wheel_delta
|
|
}
|
|
|
|
/// Check if camera was interacted with (for filtering clicks)
|
|
pub fn had_camera_interaction(&self) -> bool {
|
|
self.camera_interaction
|
|
}
|
|
|
|
/// Mark that camera was interacted with
|
|
pub fn set_camera_interaction(&mut self) {
|
|
self.camera_interaction = true;
|
|
}
|
|
}
|
|
|
|
/// Coordinate conversion utilities
|
|
pub mod coords {
|
|
use super::*;
|
|
|
|
/// Convert screen position to world position
|
|
pub fn screen_to_world(screen: ScreenPos, camera: &CameraState) -> WorldPos {
|
|
// Adjust for camera position and zoom
|
|
let world_x = (screen.x - camera.viewport_width / 2.0) / camera.zoom + camera.x;
|
|
let world_y = (screen.y - camera.viewport_height / 2.0) / camera.zoom + camera.y;
|
|
WorldPos { x: world_x, y: world_y }
|
|
}
|
|
|
|
/// Convert world position to screen position
|
|
pub fn world_to_screen(world: WorldPos, camera: &CameraState) -> ScreenPos {
|
|
let screen_x = (world.x - camera.x) * camera.zoom + camera.viewport_width / 2.0;
|
|
let screen_y = (world.y - camera.y) * camera.zoom + camera.viewport_height / 2.0;
|
|
ScreenPos { x: screen_x, y: screen_y }
|
|
}
|
|
|
|
/// Convert world position to tile coordinates
|
|
pub fn world_to_tile(world: WorldPos, map_width: u32, map_height: u32, pixel_scale: f32) -> Option<TileCoord> {
|
|
// Adjust for centered map
|
|
let half_width = (map_width as f32 * pixel_scale) / 2.0;
|
|
let half_height = (map_height as f32 * pixel_scale) / 2.0;
|
|
|
|
let adjusted_x = world.x + half_width;
|
|
let adjusted_y = world.y + half_height;
|
|
|
|
let tile_x = (adjusted_x / pixel_scale) as i32;
|
|
let tile_y = (adjusted_y / pixel_scale) as i32;
|
|
|
|
if tile_x >= 0 && tile_x < map_width as i32 && tile_y >= 0 && tile_y < map_height as i32 { Some(TileCoord { x: tile_x as u32, y: tile_y as u32 }) } else { None }
|
|
}
|
|
|
|
/// Convert tile coordinates to world position (center of tile)
|
|
pub fn tile_to_world(tile: TileCoord, map_width: u32, map_height: u32, pixel_scale: f32) -> WorldPos {
|
|
let half_width = (map_width as f32 * pixel_scale) / 2.0;
|
|
let half_height = (map_height as f32 * pixel_scale) / 2.0;
|
|
|
|
WorldPos { x: (tile.x as f32 + 0.5) * pixel_scale - half_width, y: (tile.y as f32 + 0.5) * pixel_scale - half_height }
|
|
}
|
|
|
|
/// Convert tile index to world position
|
|
pub fn tile_index_to_world(index: usize, map_width: u32, map_height: u32, pixel_scale: f32) -> WorldPos {
|
|
let tile = TileCoord::from_index(index, map_width);
|
|
tile_to_world(tile, map_width, map_height, pixel_scale)
|
|
}
|
|
|
|
/// Convert screen position directly to tile (combines screen_to_world and world_to_tile)
|
|
pub fn screen_to_tile(screen: ScreenPos, camera: &CameraState, map_width: u32, map_height: u32, pixel_scale: f32) -> Option<TileCoord> {
|
|
let world = screen_to_world(screen, camera);
|
|
world_to_tile(world, map_width, map_height, pixel_scale)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_tile_coord_conversion() {
|
|
let tile = TileCoord { x: 5, y: 3 };
|
|
let index = tile.to_index(10);
|
|
assert_eq!(index, 35); // 3 * 10 + 5
|
|
|
|
let tile2 = TileCoord::from_index(35, 10);
|
|
assert_eq!(tile2.x, 5);
|
|
assert_eq!(tile2.y, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_input_state() {
|
|
let mut state = InputState::new();
|
|
|
|
// Test key press
|
|
state.handle_event(InputEvent::KeyPress { key: KeyCode::KeyC, state: ButtonState::Pressed });
|
|
assert!(state.key_just_pressed(KeyCode::KeyC));
|
|
assert!(state.key_pressed(KeyCode::KeyC));
|
|
|
|
// Clear frame data
|
|
state.clear_frame_data();
|
|
assert!(!state.key_just_pressed(KeyCode::KeyC));
|
|
assert!(state.key_pressed(KeyCode::KeyC)); // Still pressed
|
|
|
|
// Release key
|
|
state.handle_event(InputEvent::KeyPress { key: KeyCode::KeyC, state: ButtonState::Released });
|
|
assert!(state.key_just_released(KeyCode::KeyC));
|
|
assert!(!state.key_pressed(KeyCode::KeyC));
|
|
}
|
|
|
|
#[test]
|
|
fn test_coordinate_conversion() {
|
|
let camera = CameraState { x: 100.0, y: 100.0, zoom: 2.0, viewport_width: 800.0, viewport_height: 600.0 };
|
|
|
|
let screen = ScreenPos { x: 400.0, y: 300.0 };
|
|
let world = coords::screen_to_world(screen, &camera);
|
|
assert_eq!(world.x, 100.0); // Center of screen = camera position
|
|
assert_eq!(world.y, 100.0);
|
|
|
|
// Test round trip
|
|
let screen2 = coords::world_to_screen(world, &camera);
|
|
assert!((screen2.x - screen.x).abs() < 0.001);
|
|
assert!((screen2.y - screen.y).abs() < 0.001);
|
|
}
|
|
}
|