dreamwell-engine 1.0.0

Dreamwell pure-logic engine library — transforms, hierarchy, canon pipeline, spatial math, hashing, tile rules, validation, waymark schema, material/lighting descriptors. No SpacetimeDB dependency.
Documentation
// VirtualKey + InputBinding — platform-independent key codes and action mappings.
// VirtualKey abstracts over winit KeyCode, egui Key, browser KeyboardEvent.code, etc.
// InputBinding maps VirtualKey → InputAction for rebindable controls.

use std::collections::HashMap;

use super::action::InputAction;

/// Platform-independent key/button identifiers.
/// Covers keyboard, mouse, and gamepad inputs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VirtualKey {
    // Letters
    A,
    B,
    C,
    D,
    E,
    F,
    G,
    H,
    I,
    J,
    K,
    L,
    M,
    N,
    O,
    P,
    Q,
    R,
    S,
    T,
    U,
    V,
    W,
    X,
    Y,
    Z,

    // Numbers
    Num0,
    Num1,
    Num2,
    Num3,
    Num4,
    Num5,
    Num6,
    Num7,
    Num8,
    Num9,

    // Arrows
    ArrowUp,
    ArrowDown,
    ArrowLeft,
    ArrowRight,

    // Modifiers
    ShiftLeft,
    ShiftRight,
    CtrlLeft,
    CtrlRight,
    AltLeft,
    AltRight,

    // Common
    Space,
    Enter,
    Escape,
    Tab,
    Backspace,
    Delete,
    Home,
    End,
    PageUp,
    PageDown,
    F1,
    F2,
    F3,
    F4,
    F5,
    F6,
    F7,
    F8,
    F9,
    F10,
    F11,
    F12,

    // Mouse
    MouseLeft,
    MouseRight,
    MouseMiddle,
    MouseButton4,
    MouseButton5,

    // Gamepad
    GamepadA,
    GamepadB,
    GamepadX,
    GamepadY,
    GamepadLB,
    GamepadRB,
    GamepadLT,
    GamepadRT,
    GamepadStart,
    GamepadSelect,
    GamepadDpadUp,
    GamepadDpadDown,
    GamepadDpadLeft,
    GamepadDpadRight,
    GamepadLeftStickPress,
    GamepadRightStickPress,
}

/// Rebindable input binding map. Maps VirtualKey → InputAction.
/// Multiple keys can map to the same action, but each key maps to at most one action.
/// If a single physical input should trigger context-dependent behavior (e.g., MouseLeft
/// is PrimaryAction in UI but Attack in combat), resolve that at the game layer, not here.
pub struct InputBindingMap {
    bindings: HashMap<VirtualKey, InputAction>,
}

impl InputBindingMap {
    pub fn new() -> Self {
        Self {
            bindings: HashMap::new(),
        }
    }

    /// Create default WASD + mouse bindings for tile-based and 3D movement.
    pub fn default_bindings() -> Self {
        let mut map = Self::new();

        // WASD movement
        map.bind(VirtualKey::W, InputAction::MoveForward);
        map.bind(VirtualKey::S, InputAction::MoveBackward);
        map.bind(VirtualKey::A, InputAction::MoveLeft);
        map.bind(VirtualKey::D, InputAction::MoveRight);

        // Arrow keys (alternative movement)
        map.bind(VirtualKey::ArrowUp, InputAction::MoveForward);
        map.bind(VirtualKey::ArrowDown, InputAction::MoveBackward);
        map.bind(VirtualKey::ArrowLeft, InputAction::MoveLeft);
        map.bind(VirtualKey::ArrowRight, InputAction::MoveRight);

        // Vertical movement (3D)
        map.bind(VirtualKey::Space, InputAction::Jump);
        map.bind(VirtualKey::ShiftLeft, InputAction::Sprint);

        // Camera
        map.bind(VirtualKey::MouseRight, InputAction::CameraRotate);
        map.bind(VirtualKey::MouseMiddle, InputAction::CameraPan);

        // Interaction
        map.bind(VirtualKey::MouseLeft, InputAction::PrimaryAction);
        map.bind(VirtualKey::E, InputAction::Interact);
        map.bind(VirtualKey::Escape, InputAction::Cancel);
        map.bind(VirtualKey::Enter, InputAction::Confirm);

        // Combat (MouseLeft → PrimaryAction above; game layer derives Attack from
        // PrimaryAction + combat context. Don't double-bind the same key.)
        map.bind(VirtualKey::Q, InputAction::Block);

        // UI
        map.bind(VirtualKey::I, InputAction::OpenInventory);
        map.bind(VirtualKey::M, InputAction::OpenMap);
        map.bind(VirtualKey::T, InputAction::OpenChat);

        // Quick slots (1-9)
        map.bind(VirtualKey::Num1, InputAction::QuickSlot(0));
        map.bind(VirtualKey::Num2, InputAction::QuickSlot(1));
        map.bind(VirtualKey::Num3, InputAction::QuickSlot(2));
        map.bind(VirtualKey::Num4, InputAction::QuickSlot(3));
        map.bind(VirtualKey::Num5, InputAction::QuickSlot(4));
        map.bind(VirtualKey::Num6, InputAction::QuickSlot(5));
        map.bind(VirtualKey::Num7, InputAction::QuickSlot(6));
        map.bind(VirtualKey::Num8, InputAction::QuickSlot(7));
        map.bind(VirtualKey::Num9, InputAction::QuickSlot(8));

        // System
        map.bind(VirtualKey::F11, InputAction::ToggleFullscreen);
        map.bind(VirtualKey::F12, InputAction::Screenshot);

        // Gamepad
        map.bind(VirtualKey::GamepadA, InputAction::Confirm);
        map.bind(VirtualKey::GamepadB, InputAction::Cancel);
        map.bind(VirtualKey::GamepadX, InputAction::Interact);
        map.bind(VirtualKey::GamepadY, InputAction::OpenInventory);
        map.bind(VirtualKey::GamepadRB, InputAction::Attack);
        map.bind(VirtualKey::GamepadLB, InputAction::Block);
        map.bind(VirtualKey::GamepadStart, InputAction::Pause);

        map
    }

    /// Bind a key to an action.
    pub fn bind(&mut self, key: VirtualKey, action: InputAction) {
        self.bindings.insert(key, action);
    }

    /// Remove a binding.
    pub fn unbind(&mut self, key: VirtualKey) {
        self.bindings.remove(&key);
    }

    /// Look up the action for a key.
    pub fn action_for(&self, key: VirtualKey) -> Option<InputAction> {
        self.bindings.get(&key).copied()
    }

    /// Number of bindings.
    pub fn len(&self) -> usize {
        self.bindings.len()
    }

    /// Whether the map is empty.
    pub fn is_empty(&self) -> bool {
        self.bindings.is_empty()
    }

    /// All bindings as (key, action) pairs.
    pub fn iter(&self) -> impl Iterator<Item = (&VirtualKey, &InputAction)> {
        self.bindings.iter()
    }
}

impl Default for InputBindingMap {
    fn default() -> Self {
        Self::default_bindings()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_bindings_has_wasd() {
        let map = InputBindingMap::default_bindings();
        assert_eq!(map.action_for(VirtualKey::W), Some(InputAction::MoveForward));
        assert_eq!(map.action_for(VirtualKey::S), Some(InputAction::MoveBackward));
        assert_eq!(map.action_for(VirtualKey::A), Some(InputAction::MoveLeft));
        assert_eq!(map.action_for(VirtualKey::D), Some(InputAction::MoveRight));
    }

    #[test]
    fn bind_and_unbind() {
        let mut map = InputBindingMap::new();
        map.bind(VirtualKey::X, InputAction::Attack);
        assert_eq!(map.action_for(VirtualKey::X), Some(InputAction::Attack));

        map.unbind(VirtualKey::X);
        assert_eq!(map.action_for(VirtualKey::X), None);
    }

    #[test]
    fn rebind_overwrites() {
        let mut map = InputBindingMap::new();
        map.bind(VirtualKey::Space, InputAction::Jump);
        map.bind(VirtualKey::Space, InputAction::Dodge);
        assert_eq!(map.action_for(VirtualKey::Space), Some(InputAction::Dodge));
    }

    #[test]
    fn default_bindings_nonempty() {
        let map = InputBindingMap::default_bindings();
        assert!(!map.is_empty());
        assert!(map.len() > 20);
    }

    #[test]
    fn arrow_keys_bound() {
        let map = InputBindingMap::default_bindings();
        assert_eq!(map.action_for(VirtualKey::ArrowUp), Some(InputAction::MoveForward));
        assert_eq!(map.action_for(VirtualKey::ArrowDown), Some(InputAction::MoveBackward));
        assert_eq!(map.action_for(VirtualKey::ArrowLeft), Some(InputAction::MoveLeft));
        assert_eq!(map.action_for(VirtualKey::ArrowRight), Some(InputAction::MoveRight));
    }

    #[test]
    fn quick_slots_bound() {
        let map = InputBindingMap::default_bindings();
        assert_eq!(map.action_for(VirtualKey::Num1), Some(InputAction::QuickSlot(0)));
        assert_eq!(map.action_for(VirtualKey::Num9), Some(InputAction::QuickSlot(8)));
    }

    #[test]
    fn mouse_left_maps_to_primary_action() {
        let map = InputBindingMap::default_bindings();
        // MouseLeft must map to PrimaryAction, not be silently overwritten.
        assert_eq!(map.action_for(VirtualKey::MouseLeft), Some(InputAction::PrimaryAction));
    }

    #[test]
    fn gamepad_bindings() {
        let map = InputBindingMap::default_bindings();
        assert_eq!(map.action_for(VirtualKey::GamepadA), Some(InputAction::Confirm));
        assert_eq!(map.action_for(VirtualKey::GamepadB), Some(InputAction::Cancel));
        assert_eq!(map.action_for(VirtualKey::GamepadStart), Some(InputAction::Pause));
    }
}