Skip to main content

proof_engine/editor/
input_editor.rs

1
2//! Input editor — action maps, binding remapper, gamepad configurator,
3//! dead zone tuning, macro recorder, combo system, haptic editor, input replay.
4
5use glam::{Vec2, Vec4};
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// Device types
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum InputDevice {
14    Keyboard,
15    Mouse,
16    GamepadXbox,
17    GamepadPlayStation,
18    GamepadSwitch,
19    GamepadGeneric,
20    Joystick,
21    Wheel,
22    HOTAS,
23    TouchScreen,
24    Pen,
25    Midi,
26    Custom,
27}
28
29impl InputDevice {
30    pub fn label(&self) -> &'static str {
31        match self {
32            Self::Keyboard => "Keyboard",
33            Self::Mouse => "Mouse",
34            Self::GamepadXbox => "Xbox Controller",
35            Self::GamepadPlayStation => "PlayStation Controller",
36            Self::GamepadSwitch => "Switch Pro Controller",
37            Self::GamepadGeneric => "Generic Gamepad",
38            Self::Joystick => "Joystick",
39            Self::Wheel => "Racing Wheel",
40            Self::HOTAS => "HOTAS",
41            Self::TouchScreen => "Touch Screen",
42            Self::Pen => "Pen/Stylus",
43            Self::Midi => "MIDI Controller",
44            Self::Custom => "Custom Device",
45        }
46    }
47    pub fn supports_rumble(&self) -> bool {
48        matches!(self, Self::GamepadXbox | Self::GamepadPlayStation | Self::GamepadSwitch | Self::GamepadGeneric | Self::Wheel)
49    }
50    pub fn has_analog_sticks(&self) -> bool {
51        matches!(self, Self::GamepadXbox | Self::GamepadPlayStation | Self::GamepadSwitch | Self::GamepadGeneric | Self::Joystick | Self::HOTAS)
52    }
53    pub fn has_gyro(&self) -> bool {
54        matches!(self, Self::GamepadPlayStation | Self::GamepadSwitch)
55    }
56    pub fn has_touchpad(&self) -> bool {
57        matches!(self, Self::GamepadPlayStation)
58    }
59}
60
61// ---------------------------------------------------------------------------
62// Key / button codes
63// ---------------------------------------------------------------------------
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub enum KeyCode {
67    A, B, C, D, E, F, G, H, I, J, K, L, M,
68    N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
69    Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9,
70    F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
71    F13, F14, F15, F16, F17, F18, F19, F20,
72    Space, Return, Escape, Backspace, Tab, CapsLock,
73    LShift, RShift, LCtrl, RCtrl, LAlt, RAlt, LSuper, RSuper,
74    Up, Down, Left, Right,
75    Insert, Delete, Home, End, PageUp, PageDown,
76    Numpad0, Numpad1, Numpad2, Numpad3, Numpad4,
77    Numpad5, Numpad6, Numpad7, Numpad8, Numpad9,
78    NumpadAdd, NumpadSub, NumpadMul, NumpadDiv, NumpadEnter, NumpadDecimal,
79    NumLock, ScrollLock, PrintScreen, Pause, Menu,
80    BracketLeft, BracketRight, Semicolon, Quote, Comma, Period, Slash, Backslash,
81    Grave, Minus, Equals,
82}
83
84impl KeyCode {
85    pub fn label(&self) -> &'static str {
86        match self {
87            Self::A => "A", Self::B => "B", Self::C => "C", Self::D => "D",
88            Self::E => "E", Self::F => "F", Self::G => "G", Self::H => "H",
89            Self::I => "I", Self::J => "J", Self::K => "K", Self::L => "L",
90            Self::M => "M", Self::N => "N", Self::O => "O", Self::P => "P",
91            Self::Q => "Q", Self::R => "R", Self::S => "S", Self::T => "T",
92            Self::U => "U", Self::V => "V", Self::W => "W", Self::X => "X",
93            Self::Y => "Y", Self::Z => "Z",
94            Self::Num0 => "0", Self::Num1 => "1", Self::Num2 => "2",
95            Self::Num3 => "3", Self::Num4 => "4", Self::Num5 => "5",
96            Self::Num6 => "6", Self::Num7 => "7", Self::Num8 => "8", Self::Num9 => "9",
97            Self::F1 => "F1", Self::F2 => "F2", Self::F3 => "F3", Self::F4 => "F4",
98            Self::F5 => "F5", Self::F6 => "F6", Self::F7 => "F7", Self::F8 => "F8",
99            Self::F9 => "F9", Self::F10 => "F10", Self::F11 => "F11", Self::F12 => "F12",
100            Self::F13 => "F13", Self::F14 => "F14", Self::F15 => "F15", Self::F16 => "F16",
101            Self::F17 => "F17", Self::F18 => "F18", Self::F19 => "F19", Self::F20 => "F20",
102            Self::Space => "Space", Self::Return => "Enter", Self::Escape => "Escape",
103            Self::Backspace => "Backspace", Self::Tab => "Tab", Self::CapsLock => "Caps Lock",
104            Self::LShift => "L.Shift", Self::RShift => "R.Shift",
105            Self::LCtrl => "L.Ctrl", Self::RCtrl => "R.Ctrl",
106            Self::LAlt => "L.Alt", Self::RAlt => "R.Alt",
107            Self::LSuper => "L.Super", Self::RSuper => "R.Super",
108            Self::Up => "Up", Self::Down => "Down", Self::Left => "Left", Self::Right => "Right",
109            Self::Insert => "Insert", Self::Delete => "Delete",
110            Self::Home => "Home", Self::End => "End",
111            Self::PageUp => "Page Up", Self::PageDown => "Page Down",
112            Self::Numpad0 => "Num0", Self::Numpad1 => "Num1", Self::Numpad2 => "Num2",
113            Self::Numpad3 => "Num3", Self::Numpad4 => "Num4", Self::Numpad5 => "Num5",
114            Self::Numpad6 => "Num6", Self::Numpad7 => "Num7", Self::Numpad8 => "Num8",
115            Self::Numpad9 => "Num9",
116            Self::NumpadAdd => "Num+", Self::NumpadSub => "Num-",
117            Self::NumpadMul => "Num*", Self::NumpadDiv => "Num/",
118            Self::NumpadEnter => "NumEnter", Self::NumpadDecimal => "Num.",
119            Self::NumLock => "Num Lock", Self::ScrollLock => "Scroll Lock",
120            Self::PrintScreen => "Print Screen", Self::Pause => "Pause", Self::Menu => "Menu",
121            Self::BracketLeft => "[", Self::BracketRight => "]",
122            Self::Semicolon => ";", Self::Quote => "'",
123            Self::Comma => ",", Self::Period => ".", Self::Slash => "/",
124            Self::Backslash => "\\", Self::Grave => "`",
125            Self::Minus => "-", Self::Equals => "=",
126        }
127    }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub enum MouseButton { Left, Right, Middle, X1, X2 }
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub enum MouseAxis { X, Y, ScrollX, ScrollY }
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum GamepadButton {
137    South, East, West, North,
138    LeftBumper, RightBumper,
139    LeftTrigger, RightTrigger,
140    LeftStickClick, RightStickClick,
141    DPadUp, DPadDown, DPadLeft, DPadRight,
142    Start, Select, Home, Touchpad,
143    LeftStickUp, LeftStickDown, LeftStickLeft, LeftStickRight,
144    RightStickUp, RightStickDown, RightStickLeft, RightStickRight,
145    L4, R4, L5, R5,
146}
147
148impl GamepadButton {
149    pub fn label_xbox(&self) -> &'static str {
150        match self {
151            Self::South => "A", Self::East => "B", Self::West => "X", Self::North => "Y",
152            Self::LeftBumper => "LB", Self::RightBumper => "RB",
153            Self::LeftTrigger => "LT", Self::RightTrigger => "RT",
154            Self::LeftStickClick => "LS", Self::RightStickClick => "RS",
155            Self::DPadUp => "D-Up", Self::DPadDown => "D-Down",
156            Self::DPadLeft => "D-Left", Self::DPadRight => "D-Right",
157            Self::Start => "Menu", Self::Select => "View", Self::Home => "Xbox",
158            Self::Touchpad => "Share",
159            Self::LeftStickUp => "LS Up", Self::LeftStickDown => "LS Down",
160            Self::LeftStickLeft => "LS Left", Self::LeftStickRight => "LS Right",
161            Self::RightStickUp => "RS Up", Self::RightStickDown => "RS Down",
162            Self::RightStickLeft => "RS Left", Self::RightStickRight => "RS Right",
163            Self::L4 => "P1", Self::R4 => "P2", Self::L5 => "P3", Self::R5 => "P4",
164        }
165    }
166    pub fn label_ps(&self) -> &'static str {
167        match self {
168            Self::South => "Cross", Self::East => "Circle", Self::West => "Square", Self::North => "Triangle",
169            Self::LeftBumper => "L1", Self::RightBumper => "R1",
170            Self::LeftTrigger => "L2", Self::RightTrigger => "R2",
171            Self::LeftStickClick => "L3", Self::RightStickClick => "R3",
172            Self::Start => "Options", Self::Select => "Create", Self::Home => "PS", Self::Touchpad => "Touchpad",
173            _ => self.label_xbox(),
174        }
175    }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum GamepadAxis {
180    LeftStickX, LeftStickY,
181    RightStickX, RightStickY,
182    LeftTrigger, RightTrigger,
183    GyroX, GyroY, GyroZ,
184    AccelX, AccelY, AccelZ,
185}
186
187impl GamepadAxis {
188    pub fn label(&self) -> &'static str {
189        match self {
190            Self::LeftStickX => "Left Stick X", Self::LeftStickY => "Left Stick Y",
191            Self::RightStickX => "Right Stick X", Self::RightStickY => "Right Stick Y",
192            Self::LeftTrigger => "Left Trigger", Self::RightTrigger => "Right Trigger",
193            Self::GyroX => "Gyro X", Self::GyroY => "Gyro Y", Self::GyroZ => "Gyro Z",
194            Self::AccelX => "Accel X", Self::AccelY => "Accel Y", Self::AccelZ => "Accel Z",
195        }
196    }
197}
198
199// ---------------------------------------------------------------------------
200// Input binding
201// ---------------------------------------------------------------------------
202
203#[derive(Debug, Clone, PartialEq)]
204pub enum InputBinding {
205    Key(KeyCode),
206    KeyCombo(Vec<KeyCode>),
207    MouseButton(MouseButton),
208    MouseAxis(MouseAxis, f32),
209    GamepadButton(GamepadButton),
210    GamepadAxis(GamepadAxis, f32),
211    GamepadAxisPositive(GamepadAxis),
212    GamepadAxisNegative(GamepadAxis),
213    Touch(u32),
214    Any(Vec<InputBinding>),
215    Sequence(Vec<InputBinding>),
216    Hold { binding: Box<InputBinding>, duration_secs: f32 },
217    DoubleTap { binding: Box<InputBinding>, max_interval_secs: f32 },
218    Chord(Box<InputBinding>, Box<InputBinding>),
219}
220
221impl InputBinding {
222    pub fn label(&self) -> String {
223        match self {
224            Self::Key(k) => k.label().to_string(),
225            Self::KeyCombo(keys) => keys.iter().map(|k| k.label()).collect::<Vec<_>>().join("+"),
226            Self::MouseButton(b) => format!("Mouse{:?}", b),
227            Self::MouseAxis(a, scale) => format!("Mouse{:?}*{:.1}", a, scale),
228            Self::GamepadButton(b) => b.label_xbox().to_string(),
229            Self::GamepadAxis(a, scale) => format!("{} *{:.1}", a.label(), scale),
230            Self::GamepadAxisPositive(a) => format!("{}+", a.label()),
231            Self::GamepadAxisNegative(a) => format!("{}-", a.label()),
232            Self::Touch(id) => format!("Touch{}", id),
233            Self::Any(bindings) => bindings.iter().map(|b| b.label()).collect::<Vec<_>>().join(" | "),
234            Self::Sequence(bindings) => bindings.iter().map(|b| b.label()).collect::<Vec<_>>().join(", "),
235            Self::Hold { binding, duration_secs } => format!("Hold({}, {:.1}s)", binding.label(), duration_secs),
236            Self::DoubleTap { binding, .. } => format!("2x{}", binding.label()),
237            Self::Chord(a, b) => format!("{}+{}", a.label(), b.label()),
238        }
239    }
240
241    pub fn is_analog(&self) -> bool {
242        matches!(self, Self::MouseAxis(..) | Self::GamepadAxis(..) | Self::GamepadAxisPositive(..) | Self::GamepadAxisNegative(..))
243    }
244}
245
246// ---------------------------------------------------------------------------
247// Action value types
248// ---------------------------------------------------------------------------
249
250#[derive(Debug, Clone, Copy, PartialEq)]
251pub enum ActionValueType { Bool, Float, Vec2, Vec3 }
252
253impl ActionValueType {
254    pub fn label(&self) -> &'static str {
255        match self {
256            Self::Bool => "Digital",
257            Self::Float => "Axis 1D",
258            Self::Vec2 => "Axis 2D",
259            Self::Vec3 => "Axis 3D",
260        }
261    }
262}
263
264#[derive(Debug, Clone, Copy)]
265pub enum ActionValue {
266    Bool(bool),
267    Float(f32),
268    Vec2(Vec2),
269    Vec3(glam::Vec3),
270}
271
272impl ActionValue {
273    pub fn as_bool(&self) -> bool {
274        match self {
275            Self::Bool(v) => *v,
276            Self::Float(v) => *v != 0.0,
277            Self::Vec2(v) => v.length() > 0.0,
278            Self::Vec3(v) => v.length() > 0.0,
279        }
280    }
281    pub fn as_float(&self) -> f32 {
282        match self {
283            Self::Bool(v) => if *v { 1.0 } else { 0.0 },
284            Self::Float(v) => *v,
285            Self::Vec2(v) => v.length(),
286            Self::Vec3(v) => v.length(),
287        }
288    }
289}
290
291// ---------------------------------------------------------------------------
292// Input modifiers
293// ---------------------------------------------------------------------------
294
295#[derive(Debug, Clone)]
296pub enum InputModifier {
297    DeadZone { lower: f32, upper: f32, kind: DeadZoneKind },
298    Scale(f32),
299    ScaleByDeltaTime,
300    Negate,
301    Clamp { min: f32, max: f32 },
302    Normalize,
303    Smooth { speed: f32 },
304    Accumulate,
305    RelativeInput,
306    ResponseCurve { kind: ResponseCurveKind, exponent: f32 },
307    SwizzleAxes(SwizzleOrder),
308    InvertAxis { x: bool, y: bool, z: bool },
309    SensitivityMultiplier { x: f32, y: f32 },
310    AimAssist { strength: f32, radius: f32 },
311    FovScale { base_fov: f32 },
312    CustomCode(String),
313}
314
315#[derive(Debug, Clone, Copy, PartialEq)]
316pub enum DeadZoneKind { Axial, Radial, Hysteresis }
317
318#[derive(Debug, Clone, Copy, PartialEq)]
319pub enum ResponseCurveKind { Linear, Exponential, Logarithmic, SineLike, Custom }
320
321#[derive(Debug, Clone, Copy, PartialEq)]
322pub enum SwizzleOrder { XYZ, XZY, YXZ, YZX, ZXY, ZYX }
323
324impl InputModifier {
325    pub fn label(&self) -> &'static str {
326        match self {
327            Self::DeadZone { .. } => "Dead Zone",
328            Self::Scale(_) => "Scale",
329            Self::ScaleByDeltaTime => "Scale by Delta Time",
330            Self::Negate => "Negate",
331            Self::Clamp { .. } => "Clamp",
332            Self::Normalize => "Normalize",
333            Self::Smooth { .. } => "Smooth",
334            Self::Accumulate => "Accumulate",
335            Self::RelativeInput => "Relative Input",
336            Self::ResponseCurve { .. } => "Response Curve",
337            Self::SwizzleAxes(_) => "Swizzle Axes",
338            Self::InvertAxis { .. } => "Invert Axis",
339            Self::SensitivityMultiplier { .. } => "Sensitivity",
340            Self::AimAssist { .. } => "Aim Assist",
341            Self::FovScale { .. } => "FOV Scale",
342            Self::CustomCode(_) => "Custom",
343        }
344    }
345
346    pub fn apply_float(&self, value: f32, dt: f32) -> f32 {
347        match self {
348            Self::DeadZone { lower, upper, kind: DeadZoneKind::Axial } => {
349                let abs = value.abs();
350                if abs < *lower { 0.0 }
351                else if abs > *upper { value.signum() }
352                else { value.signum() * (abs - lower) / (upper - lower) }
353            }
354            Self::Scale(s) => value * s,
355            Self::ScaleByDeltaTime => value * dt,
356            Self::Negate => -value,
357            Self::Clamp { min, max } => value.clamp(*min, *max),
358            Self::Normalize => if value.abs() > 0.001 { value.signum() } else { 0.0 },
359            Self::ResponseCurve { kind: ResponseCurveKind::Exponential, exponent, .. } => {
360                value.signum() * value.abs().powf(*exponent)
361            }
362            _ => value,
363        }
364    }
365}
366
367// ---------------------------------------------------------------------------
368// Input triggers
369// ---------------------------------------------------------------------------
370
371#[derive(Debug, Clone)]
372pub enum InputTrigger {
373    Down,
374    Pressed,
375    Released,
376    Hold { hold_time: f32, is_one_shot: bool },
377    HoldAndRelease { hold_time: f32 },
378    Tap { release_time_limit: f32 },
379    Pulse { interval: f32, trigger_on_start: bool, trigger_limit: u32 },
380    ChordAction { chord_action: String },
381    Combo { sequence: Vec<String>, max_time_between: f32 },
382}
383
384impl InputTrigger {
385    pub fn label(&self) -> &'static str {
386        match self {
387            Self::Down => "Down",
388            Self::Pressed => "Pressed",
389            Self::Released => "Released",
390            Self::Hold { .. } => "Hold",
391            Self::HoldAndRelease { .. } => "Hold & Release",
392            Self::Tap { .. } => "Tap",
393            Self::Pulse { .. } => "Pulse",
394            Self::ChordAction { .. } => "Chord Action",
395            Self::Combo { .. } => "Combo",
396        }
397    }
398}
399
400// ---------------------------------------------------------------------------
401// Input action
402// ---------------------------------------------------------------------------
403
404#[derive(Debug, Clone)]
405pub struct InputAction {
406    pub name: String,
407    pub value_type: ActionValueType,
408    pub bindings: Vec<ActionBinding>,
409    pub consume_input: bool,
410    pub consume_lower_priority: bool,
411    pub description: String,
412    pub category: String,
413    pub icon: String,
414}
415
416#[derive(Debug, Clone)]
417pub struct ActionBinding {
418    pub binding: InputBinding,
419    pub device: InputDevice,
420    pub modifiers: Vec<InputModifier>,
421    pub triggers: Vec<InputTrigger>,
422    pub platform_filter: Vec<Platform>,
423    pub override_context: Option<String>,
424}
425
426#[derive(Debug, Clone, Copy, PartialEq)]
427pub enum Platform { PC, XboxConsole, PlayStation, Switch, Mobile, VR, All }
428
429impl ActionBinding {
430    pub fn new(binding: InputBinding, device: InputDevice) -> Self {
431        Self {
432            binding,
433            device,
434            modifiers: Vec::new(),
435            triggers: vec![InputTrigger::Pressed],
436            platform_filter: vec![Platform::All],
437            override_context: None,
438        }
439    }
440    pub fn with_modifier(mut self, modifier: InputModifier) -> Self {
441        self.modifiers.push(modifier);
442        self
443    }
444    pub fn with_trigger(mut self, trigger: InputTrigger) -> Self {
445        self.triggers = vec![trigger];
446        self
447    }
448    pub fn analog_binding(binding: InputBinding, device: InputDevice) -> Self {
449        Self {
450            binding,
451            device,
452            modifiers: vec![
453                InputModifier::DeadZone { lower: 0.15, upper: 1.0, kind: DeadZoneKind::Radial },
454                InputModifier::ResponseCurve { kind: ResponseCurveKind::Exponential, exponent: 1.5 },
455            ],
456            triggers: vec![InputTrigger::Down],
457            platform_filter: vec![Platform::All],
458            override_context: None,
459        }
460    }
461}
462
463impl InputAction {
464    pub fn new(name: &str, value_type: ActionValueType) -> Self {
465        Self {
466            name: name.to_string(),
467            value_type,
468            bindings: Vec::new(),
469            consume_input: true,
470            consume_lower_priority: false,
471            description: String::new(),
472            category: "General".to_string(),
473            icon: String::new(),
474        }
475    }
476    pub fn with_binding(mut self, binding: ActionBinding) -> Self {
477        self.bindings.push(binding);
478        self
479    }
480    pub fn with_description(mut self, desc: &str) -> Self {
481        self.description = desc.to_string();
482        self
483    }
484    pub fn with_category(mut self, cat: &str) -> Self {
485        self.category = cat.to_string();
486        self
487    }
488}
489
490// ---------------------------------------------------------------------------
491// Action map context
492// ---------------------------------------------------------------------------
493
494#[derive(Debug, Clone)]
495pub struct ActionMapContext {
496    pub name: String,
497    pub priority: i32,
498    pub block_lower_priority: bool,
499    pub actions: Vec<InputAction>,
500    pub enabled: bool,
501    pub description: String,
502}
503
504impl ActionMapContext {
505    pub fn new(name: &str, priority: i32) -> Self {
506        Self {
507            name: name.to_string(),
508            priority,
509            block_lower_priority: false,
510            actions: Vec::new(),
511            enabled: true,
512            description: String::new(),
513        }
514    }
515    pub fn with_action(mut self, action: InputAction) -> Self {
516        self.actions.push(action);
517        self
518    }
519    pub fn find_action(&self, name: &str) -> Option<&InputAction> {
520        self.actions.iter().find(|a| a.name == name)
521    }
522    pub fn find_action_mut(&mut self, name: &str) -> Option<&mut InputAction> {
523        self.actions.iter_mut().find(|a| a.name == name)
524    }
525    pub fn action_names(&self) -> Vec<&str> {
526        self.actions.iter().map(|a| a.name.as_str()).collect()
527    }
528}
529
530// ---------------------------------------------------------------------------
531// Dead zone visualizer data
532// ---------------------------------------------------------------------------
533
534#[derive(Debug, Clone)]
535pub struct DeadZoneConfig {
536    pub inner_radius: f32,
537    pub outer_radius: f32,
538    pub axial_x: f32,
539    pub axial_y: f32,
540    pub kind: DeadZoneKind,
541    pub snap_to_axis: bool,
542    pub snap_threshold: f32,
543    pub anti_deadzone: f32,
544}
545
546impl Default for DeadZoneConfig {
547    fn default() -> Self {
548        Self {
549            inner_radius: 0.15,
550            outer_radius: 0.95,
551            axial_x: 0.15,
552            axial_y: 0.15,
553            kind: DeadZoneKind::Radial,
554            snap_to_axis: false,
555            snap_threshold: 0.1,
556            anti_deadzone: 0.0,
557        }
558    }
559}
560
561impl DeadZoneConfig {
562    pub fn apply_radial(&self, raw: Vec2) -> Vec2 {
563        let len = raw.length();
564        if len < self.inner_radius { return Vec2::ZERO; }
565        if len > self.outer_radius { return raw / len; }
566        let rescaled = (len - self.inner_radius) / (self.outer_radius - self.inner_radius);
567        raw / len * rescaled
568    }
569    pub fn apply_axial(&self, raw: Vec2) -> Vec2 {
570        let x = if raw.x.abs() < self.axial_x { 0.0 } else { (raw.x.abs() - self.axial_x) / (1.0 - self.axial_x) * raw.x.signum() };
571        let y = if raw.y.abs() < self.axial_y { 0.0 } else { (raw.y.abs() - self.axial_y) / (1.0 - self.axial_y) * raw.y.signum() };
572        Vec2::new(x, y)
573    }
574    pub fn apply(&self, raw: Vec2) -> Vec2 {
575        match self.kind {
576            DeadZoneKind::Radial => self.apply_radial(raw),
577            DeadZoneKind::Axial => self.apply_axial(raw),
578            DeadZoneKind::Hysteresis => self.apply_radial(raw),
579        }
580    }
581}
582
583// ---------------------------------------------------------------------------
584// Sensitivity profiles
585// ---------------------------------------------------------------------------
586
587#[derive(Debug, Clone)]
588pub struct SensitivityProfile {
589    pub name: String,
590    pub mouse_x: f32,
591    pub mouse_y: f32,
592    pub right_stick_x: f32,
593    pub right_stick_y: f32,
594    pub aim_mouse_x: f32,
595    pub aim_mouse_y: f32,
596    pub aim_stick_x: f32,
597    pub aim_stick_y: f32,
598    pub invert_y_mouse: bool,
599    pub invert_y_stick: bool,
600    pub invert_x_stick: bool,
601    pub ads_sensitivity_multiplier: f32,
602    pub acceleration: f32,
603    pub input_lag_compensation_ms: f32,
604    pub mouse_smoothing: f32,
605    pub raw_input: bool,
606}
607
608impl Default for SensitivityProfile {
609    fn default() -> Self {
610        Self {
611            name: "Default".to_string(),
612            mouse_x: 1.0, mouse_y: 1.0,
613            right_stick_x: 1.0, right_stick_y: 1.0,
614            aim_mouse_x: 0.6, aim_mouse_y: 0.6,
615            aim_stick_x: 0.6, aim_stick_y: 0.6,
616            invert_y_mouse: false,
617            invert_y_stick: false,
618            invert_x_stick: false,
619            ads_sensitivity_multiplier: 0.7,
620            acceleration: 0.0,
621            input_lag_compensation_ms: 0.0,
622            mouse_smoothing: 0.0,
623            raw_input: true,
624        }
625    }
626}
627
628impl SensitivityProfile {
629    pub fn esports() -> Self {
630        Self { name: "eSports".to_string(), mouse_x: 0.5, mouse_y: 0.5, raw_input: true, mouse_smoothing: 0.0, ..Default::default() }
631    }
632    pub fn casual() -> Self {
633        Self { name: "Casual".to_string(), mouse_x: 1.5, mouse_y: 1.5, mouse_smoothing: 0.3, ..Default::default() }
634    }
635    pub fn console() -> Self {
636        Self { name: "Console".to_string(), right_stick_x: 2.0, right_stick_y: 1.8, aim_stick_x: 1.2, aim_stick_y: 1.2, ..Default::default() }
637    }
638}
639
640// ---------------------------------------------------------------------------
641// Haptic feedback
642// ---------------------------------------------------------------------------
643
644#[derive(Debug, Clone, Copy, PartialEq)]
645pub enum HapticEffectKind {
646    Constant,
647    Sine,
648    Square,
649    Triangle,
650    SawUp,
651    SawDown,
652    Custom,
653    Rumble,
654    AdaptiveTriggerFeedback,
655    AdaptiveTriggerWeapon,
656    AdaptiveTriggerBow,
657}
658
659impl HapticEffectKind {
660    pub fn label(&self) -> &'static str {
661        match self {
662            Self::Constant => "Constant",
663            Self::Sine => "Sine",
664            Self::Square => "Square",
665            Self::Triangle => "Triangle",
666            Self::SawUp => "Saw Up",
667            Self::SawDown => "Saw Down",
668            Self::Custom => "Custom",
669            Self::Rumble => "Rumble",
670            Self::AdaptiveTriggerFeedback => "Adaptive: Feedback",
671            Self::AdaptiveTriggerWeapon => "Adaptive: Weapon",
672            Self::AdaptiveTriggerBow => "Adaptive: Bow",
673        }
674    }
675}
676
677#[derive(Debug, Clone)]
678pub struct HapticEffect {
679    pub name: String,
680    pub kind: HapticEffectKind,
681    pub duration_secs: f32,
682    pub low_freq_motor: f32,
683    pub high_freq_motor: f32,
684    pub left_trigger_intensity: f32,
685    pub right_trigger_intensity: f32,
686    pub start_delay_secs: f32,
687    pub fade_in_secs: f32,
688    pub fade_out_secs: f32,
689    pub frequency_hz: f32,
690    pub attack_intensity: f32,
691    pub sustain_intensity: f32,
692    pub release_intensity: f32,
693}
694
695impl HapticEffect {
696    pub fn new(name: &str, kind: HapticEffectKind) -> Self {
697        Self {
698            name: name.to_string(),
699            kind,
700            duration_secs: 0.3,
701            low_freq_motor: 0.5,
702            high_freq_motor: 0.5,
703            left_trigger_intensity: 0.0,
704            right_trigger_intensity: 0.0,
705            start_delay_secs: 0.0,
706            fade_in_secs: 0.0,
707            fade_out_secs: 0.1,
708            frequency_hz: 60.0,
709            attack_intensity: 1.0,
710            sustain_intensity: 0.7,
711            release_intensity: 0.0,
712        }
713    }
714
715    pub fn gun_shot() -> Self {
716        let mut e = Self::new("GunShot", HapticEffectKind::Rumble);
717        e.duration_secs = 0.15;
718        e.low_freq_motor = 0.9;
719        e.high_freq_motor = 0.6;
720        e.fade_out_secs = 0.08;
721        e
722    }
723
724    pub fn explosion() -> Self {
725        let mut e = Self::new("Explosion", HapticEffectKind::Rumble);
726        e.duration_secs = 0.6;
727        e.low_freq_motor = 1.0;
728        e.high_freq_motor = 0.4;
729        e.fade_in_secs = 0.05;
730        e.fade_out_secs = 0.3;
731        e
732    }
733
734    pub fn footstep() -> Self {
735        let mut e = Self::new("Footstep", HapticEffectKind::Sine);
736        e.duration_secs = 0.08;
737        e.low_freq_motor = 0.3;
738        e.high_freq_motor = 0.2;
739        e
740    }
741
742    pub fn ui_click() -> Self {
743        let mut e = Self::new("UIClick", HapticEffectKind::Constant);
744        e.duration_secs = 0.04;
745        e.low_freq_motor = 0.1;
746        e.high_freq_motor = 0.4;
747        e
748    }
749
750    pub fn intensity_at_time(&self, t: f32) -> f32 {
751        if t < 0.0 || t > self.duration_secs { return 0.0; }
752        let fade_in_end = self.fade_in_secs;
753        let fade_out_start = self.duration_secs - self.fade_out_secs;
754        let envelope = if t < fade_in_end {
755            t / fade_in_end.max(0.001)
756        } else if t > fade_out_start {
757            1.0 - (t - fade_out_start) / self.fade_out_secs.max(0.001)
758        } else {
759            1.0
760        };
761        let wave = match self.kind {
762            HapticEffectKind::Sine => (t * self.frequency_hz * std::f32::consts::TAU).sin() * 0.5 + 0.5,
763            HapticEffectKind::Square => if (t * self.frequency_hz).fract() < 0.5 { 1.0 } else { 0.0 },
764            HapticEffectKind::Triangle => {
765                let phase = (t * self.frequency_hz).fract();
766                if phase < 0.5 { phase * 2.0 } else { 2.0 - phase * 2.0 }
767            }
768            _ => 1.0,
769        };
770        envelope * wave * self.sustain_intensity
771    }
772}
773
774// ---------------------------------------------------------------------------
775// Input macro system
776// ---------------------------------------------------------------------------
777
778#[derive(Debug, Clone)]
779pub struct InputMacroStep {
780    pub action_name: String,
781    pub value: f32,
782    pub delay_before_ms: u32,
783    pub duration_ms: u32,
784    pub repeat: u32,
785}
786
787#[derive(Debug, Clone)]
788pub struct InputMacro {
789    pub name: String,
790    pub description: String,
791    pub steps: Vec<InputMacroStep>,
792    pub loop_count: u32,
793    pub allow_during_gameplay: bool,
794    pub trigger_binding: Option<InputBinding>,
795    pub recording: bool,
796    pub playback_speed: f32,
797}
798
799impl InputMacro {
800    pub fn new(name: &str) -> Self {
801        Self {
802            name: name.to_string(),
803            description: String::new(),
804            steps: Vec::new(),
805            loop_count: 1,
806            allow_during_gameplay: false,
807            trigger_binding: None,
808            recording: false,
809            playback_speed: 1.0,
810        }
811    }
812    pub fn total_duration_ms(&self) -> u32 {
813        self.steps.iter().map(|s| s.delay_before_ms + s.duration_ms * s.repeat.max(1)).sum()
814    }
815    pub fn add_step(&mut self, action: &str, value: f32, delay_ms: u32, dur_ms: u32) {
816        self.steps.push(InputMacroStep {
817            action_name: action.to_string(),
818            value,
819            delay_before_ms: delay_ms,
820            duration_ms: dur_ms,
821            repeat: 1,
822        });
823    }
824}
825
826// ---------------------------------------------------------------------------
827// Combo system
828// ---------------------------------------------------------------------------
829
830#[derive(Debug, Clone)]
831pub struct ComboInput {
832    pub action: String,
833    pub min_value: f32,
834    pub max_hold_ms: Option<u32>,
835}
836
837#[derive(Debug, Clone)]
838pub struct ComboDefinition {
839    pub name: String,
840    pub sequence: Vec<ComboInput>,
841    pub max_gap_ms: u32,
842    pub output_action: String,
843    pub output_value: f32,
844    pub priority: i32,
845    pub enabled: bool,
846}
847
848impl ComboDefinition {
849    pub fn new(name: &str, output: &str) -> Self {
850        Self {
851            name: name.to_string(),
852            sequence: Vec::new(),
853            max_gap_ms: 300,
854            output_action: output.to_string(),
855            output_value: 1.0,
856            priority: 0,
857            enabled: true,
858        }
859    }
860    pub fn add_input(mut self, action: &str, min_value: f32) -> Self {
861        self.sequence.push(ComboInput { action: action.to_string(), min_value, max_hold_ms: None });
862        self
863    }
864    pub fn input_count(&self) -> usize { self.sequence.len() }
865}
866
867// ---------------------------------------------------------------------------
868// Input event log for replay
869// ---------------------------------------------------------------------------
870
871#[derive(Debug, Clone)]
872pub struct InputEvent {
873    pub timestamp_ms: f64,
874    pub device: InputDevice,
875    pub action_name: String,
876    pub value: f32,
877    pub value2: f32,
878    pub frame: u64,
879}
880
881#[derive(Debug, Clone, Default)]
882pub struct InputReplay {
883    pub events: Vec<InputEvent>,
884    pub duration_ms: f64,
885    pub recording: bool,
886    pub playing: bool,
887    pub playback_pos_ms: f64,
888    pub loop_replay: bool,
889    pub playback_speed: f32,
890}
891
892impl InputReplay {
893    pub fn start_recording(&mut self) { self.recording = true; self.events.clear(); }
894    pub fn stop_recording(&mut self) {
895        self.recording = false;
896        self.duration_ms = self.events.last().map(|e| e.timestamp_ms).unwrap_or(0.0);
897    }
898    pub fn start_playback(&mut self) { self.playing = true; self.playback_pos_ms = 0.0; }
899    pub fn stop_playback(&mut self) { self.playing = false; }
900    pub fn record_event(&mut self, event: InputEvent) {
901        if self.recording { self.events.push(event); }
902    }
903    pub fn advance(&mut self, dt_ms: f64) {
904        if !self.playing { return; }
905        self.playback_pos_ms += dt_ms * self.playback_speed as f64;
906        if self.playback_pos_ms >= self.duration_ms {
907            if self.loop_replay { self.playback_pos_ms -= self.duration_ms; }
908            else { self.playing = false; }
909        }
910    }
911    pub fn current_events(&self) -> Vec<&InputEvent> {
912        let t = self.playback_pos_ms;
913        self.events.iter().filter(|e| (e.timestamp_ms - t).abs() < 16.0).collect()
914    }
915    pub fn event_count(&self) -> usize { self.events.len() }
916}
917
918// ---------------------------------------------------------------------------
919// Virtual cursor / pointer emulation
920// ---------------------------------------------------------------------------
921
922#[derive(Debug, Clone)]
923pub struct VirtualCursor {
924    pub position: Vec2,
925    pub velocity: Vec2,
926    pub acceleration: f32,
927    pub max_speed: f32,
928    pub friction: f32,
929    pub deadzone: f32,
930    pub screen_size: Vec2,
931    pub enabled: bool,
932    pub stick_axis: (GamepadAxis, GamepadAxis),
933}
934
935impl Default for VirtualCursor {
936    fn default() -> Self {
937        Self {
938            position: Vec2::new(960.0, 540.0),
939            velocity: Vec2::ZERO,
940            acceleration: 1200.0,
941            max_speed: 800.0,
942            friction: 8.0,
943            deadzone: 0.15,
944            screen_size: Vec2::new(1920.0, 1080.0),
945            enabled: false,
946            stick_axis: (GamepadAxis::RightStickX, GamepadAxis::RightStickY),
947        }
948    }
949}
950
951impl VirtualCursor {
952    pub fn update(&mut self, stick: Vec2, dt: f32) {
953        let len = stick.length();
954        if len < self.deadzone {
955            self.velocity *= 1.0 - self.friction * dt;
956        } else {
957            let dir = stick / len;
958            let t = (len - self.deadzone) / (1.0 - self.deadzone);
959            self.velocity += dir * self.acceleration * t * dt;
960            let speed = self.velocity.length();
961            if speed > self.max_speed {
962                self.velocity = self.velocity / speed * self.max_speed;
963            }
964        }
965        self.position += self.velocity * dt;
966        self.position.x = self.position.x.clamp(0.0, self.screen_size.x);
967        self.position.y = self.position.y.clamp(0.0, self.screen_size.y);
968    }
969}
970
971// ---------------------------------------------------------------------------
972// Gesture recognizer
973// ---------------------------------------------------------------------------
974
975#[derive(Debug, Clone, Copy, PartialEq)]
976pub enum GestureKind {
977    Tap,
978    DoubleTap,
979    LongPress,
980    Swipe,
981    Pinch,
982    Rotate,
983    Pan,
984    Flick,
985    TwoFingerTap,
986    ThreeFingerTap,
987    EdgeSwipe,
988    ForceTap,
989}
990
991impl GestureKind {
992    pub fn label(&self) -> &'static str {
993        match self {
994            Self::Tap => "Tap",
995            Self::DoubleTap => "Double Tap",
996            Self::LongPress => "Long Press",
997            Self::Swipe => "Swipe",
998            Self::Pinch => "Pinch",
999            Self::Rotate => "Rotate",
1000            Self::Pan => "Pan",
1001            Self::Flick => "Flick",
1002            Self::TwoFingerTap => "Two-Finger Tap",
1003            Self::ThreeFingerTap => "Three-Finger Tap",
1004            Self::EdgeSwipe => "Edge Swipe",
1005            Self::ForceTap => "Force Tap",
1006        }
1007    }
1008    pub fn min_touch_points(&self) -> u32 {
1009        match self {
1010            Self::Pinch | Self::Rotate | Self::TwoFingerTap => 2,
1011            Self::ThreeFingerTap => 3,
1012            _ => 1,
1013        }
1014    }
1015}
1016
1017#[derive(Debug, Clone)]
1018pub struct GestureBinding {
1019    pub gesture: GestureKind,
1020    pub action: String,
1021    pub min_velocity: f32,
1022    pub max_duration_secs: f32,
1023    pub direction_filter: Option<Vec2>,
1024    pub direction_tolerance_deg: f32,
1025}
1026
1027// ---------------------------------------------------------------------------
1028// Accessibility options
1029// ---------------------------------------------------------------------------
1030
1031#[derive(Debug, Clone)]
1032pub struct AccessibilitySettings {
1033    pub sticky_keys: bool,
1034    pub slow_keys: bool,
1035    pub slow_keys_delay_ms: u32,
1036    pub toggle_keys: bool,
1037    pub filter_keys: bool,
1038    pub filter_keys_repeat_delay_ms: u32,
1039    pub mouse_keys: bool,
1040    pub mouse_keys_speed: f32,
1041    pub high_contrast_cursor: bool,
1042    pub cursor_size_scale: f32,
1043    pub single_button_mode: bool,
1044    pub auto_sprint: bool,
1045    pub aim_assist_strength: f32,
1046    pub auto_aim: bool,
1047    pub hold_to_toggle_aim: bool,
1048    pub simplified_controls: bool,
1049    pub button_remapping_enabled: bool,
1050    pub hold_alternatives: HashMap<String, String>,
1051    pub color_blind_mode: ColorBlindMode,
1052    pub text_size_scale: f32,
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq)]
1056pub enum ColorBlindMode { None, Protanopia, Deuteranopia, Tritanopia, Monochromacy }
1057
1058impl ColorBlindMode {
1059    pub fn label(&self) -> &'static str {
1060        match self {
1061            Self::None => "None",
1062            Self::Protanopia => "Protanopia (Red-Green)",
1063            Self::Deuteranopia => "Deuteranopia (Green-Red)",
1064            Self::Tritanopia => "Tritanopia (Blue-Yellow)",
1065            Self::Monochromacy => "Monochromacy",
1066        }
1067    }
1068}
1069
1070impl Default for AccessibilitySettings {
1071    fn default() -> Self {
1072        Self {
1073            sticky_keys: false,
1074            slow_keys: false,
1075            slow_keys_delay_ms: 500,
1076            toggle_keys: false,
1077            filter_keys: false,
1078            filter_keys_repeat_delay_ms: 200,
1079            mouse_keys: false,
1080            mouse_keys_speed: 1.0,
1081            high_contrast_cursor: false,
1082            cursor_size_scale: 1.0,
1083            single_button_mode: false,
1084            auto_sprint: false,
1085            aim_assist_strength: 0.0,
1086            auto_aim: false,
1087            hold_to_toggle_aim: false,
1088            simplified_controls: false,
1089            button_remapping_enabled: true,
1090            hold_alternatives: HashMap::new(),
1091            color_blind_mode: ColorBlindMode::None,
1092            text_size_scale: 1.0,
1093        }
1094    }
1095}
1096
1097// ---------------------------------------------------------------------------
1098// Input device state (runtime)
1099// ---------------------------------------------------------------------------
1100
1101#[derive(Debug, Clone)]
1102pub struct GamepadState {
1103    pub device_id: u32,
1104    pub device: InputDevice,
1105    pub connected: bool,
1106    pub left_stick: Vec2,
1107    pub right_stick: Vec2,
1108    pub left_trigger: f32,
1109    pub right_trigger: f32,
1110    pub buttons_pressed: std::collections::HashSet<u32>,
1111    pub gyro: glam::Vec3,
1112    pub accel: glam::Vec3,
1113    pub touchpad_pos: Vec2,
1114    pub touchpad_pressed: bool,
1115    pub battery_level: f32,
1116    pub is_wireless: bool,
1117}
1118
1119impl Default for GamepadState {
1120    fn default() -> Self {
1121        Self {
1122            device_id: 0,
1123            device: InputDevice::GamepadGeneric,
1124            connected: false,
1125            left_stick: Vec2::ZERO,
1126            right_stick: Vec2::ZERO,
1127            left_trigger: 0.0,
1128            right_trigger: 0.0,
1129            buttons_pressed: std::collections::HashSet::new(),
1130            gyro: glam::Vec3::ZERO,
1131            accel: glam::Vec3::ZERO,
1132            touchpad_pos: Vec2::ZERO,
1133            touchpad_pressed: false,
1134            battery_level: 1.0,
1135            is_wireless: false,
1136        }
1137    }
1138}
1139
1140impl GamepadState {
1141    pub fn synthetic_idle() -> Self {
1142        Self {
1143            device_id: 0,
1144            device: InputDevice::GamepadXbox,
1145            connected: true,
1146            battery_level: 0.85,
1147            ..Default::default()
1148        }
1149    }
1150}
1151
1152#[derive(Debug, Clone, Default)]
1153pub struct KeyboardState {
1154    pub keys_down: std::collections::HashSet<u32>,
1155    pub keys_just_pressed: std::collections::HashSet<u32>,
1156    pub keys_just_released: std::collections::HashSet<u32>,
1157    pub shift: bool,
1158    pub ctrl: bool,
1159    pub alt: bool,
1160    pub super_key: bool,
1161}
1162
1163#[derive(Debug, Clone, Default)]
1164pub struct MouseState {
1165    pub position: Vec2,
1166    pub delta: Vec2,
1167    pub scroll: Vec2,
1168    pub left: bool,
1169    pub right: bool,
1170    pub middle: bool,
1171    pub x1: bool,
1172    pub x2: bool,
1173    pub just_pressed: std::collections::HashSet<u32>,
1174    pub just_released: std::collections::HashSet<u32>,
1175    pub captured: bool,
1176    pub dpi: u32,
1177}
1178
1179// ---------------------------------------------------------------------------
1180// Binding conflict detection
1181// ---------------------------------------------------------------------------
1182
1183#[derive(Debug, Clone)]
1184pub struct BindingConflict {
1185    pub context_a: String,
1186    pub action_a: String,
1187    pub context_b: String,
1188    pub action_b: String,
1189    pub conflicting_binding: String,
1190    pub severity: ConflictSeverity,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq)]
1194pub enum ConflictSeverity { Warning, Error }
1195
1196// ---------------------------------------------------------------------------
1197// Full input asset (serializable config)
1198// ---------------------------------------------------------------------------
1199
1200#[derive(Debug, Clone)]
1201pub struct InputAsset {
1202    pub name: String,
1203    pub contexts: Vec<ActionMapContext>,
1204    pub haptic_effects: Vec<HapticEffect>,
1205    pub macros: Vec<InputMacro>,
1206    pub combos: Vec<ComboDefinition>,
1207    pub sensitivity_profiles: Vec<SensitivityProfile>,
1208    pub active_sensitivity_profile: usize,
1209    pub deadzone_configs: HashMap<GamepadAxis, DeadZoneConfig>,
1210    pub accessibility: AccessibilitySettings,
1211    pub gesture_bindings: Vec<GestureBinding>,
1212    pub virtual_cursor: VirtualCursor,
1213    pub replay: InputReplay,
1214}
1215
1216impl InputAsset {
1217    pub fn default_fps() -> Self {
1218        let gameplay = ActionMapContext::new("Gameplay", 100)
1219            .with_action(
1220                InputAction::new("Move", ActionValueType::Vec2)
1221                    .with_category("Movement")
1222                    .with_description("WASD / Left Stick movement")
1223                    .with_binding(ActionBinding::new(InputBinding::KeyCombo(vec![KeyCode::W]), InputDevice::Keyboard))
1224                    .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::LeftStickY, 1.0), InputDevice::GamepadXbox))
1225            )
1226            .with_action(
1227                InputAction::new("Look", ActionValueType::Vec2)
1228                    .with_category("Movement")
1229                    .with_description("Mouse / Right Stick look")
1230                    .with_binding(ActionBinding::new(InputBinding::MouseAxis(MouseAxis::X, 1.0), InputDevice::Mouse))
1231                    .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::RightStickX, 1.0), InputDevice::GamepadXbox))
1232            )
1233            .with_action(
1234                InputAction::new("Jump", ActionValueType::Bool)
1235                    .with_category("Movement")
1236                    .with_description("Jump / A button")
1237                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Space), InputDevice::Keyboard))
1238                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::South), InputDevice::GamepadXbox))
1239            )
1240            .with_action(
1241                InputAction::new("Sprint", ActionValueType::Bool)
1242                    .with_category("Movement")
1243                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::LShift), InputDevice::Keyboard)
1244                        .with_trigger(InputTrigger::Down))
1245                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::LeftStickClick), InputDevice::GamepadXbox))
1246            )
1247            .with_action(
1248                InputAction::new("Crouch", ActionValueType::Bool)
1249                    .with_category("Movement")
1250                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::LCtrl), InputDevice::Keyboard))
1251                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::RightStickClick), InputDevice::GamepadXbox))
1252            )
1253            .with_action(
1254                InputAction::new("Fire", ActionValueType::Bool)
1255                    .with_category("Combat")
1256                    .with_binding(ActionBinding::new(InputBinding::MouseButton(MouseButton::Left), InputDevice::Mouse)
1257                        .with_trigger(InputTrigger::Down))
1258                    .with_binding(ActionBinding::new(InputBinding::GamepadAxis(GamepadAxis::RightTrigger, 1.0), InputDevice::GamepadXbox)
1259                        .with_modifier(InputModifier::DeadZone { lower: 0.3, upper: 1.0, kind: DeadZoneKind::Axial }))
1260            )
1261            .with_action(
1262                InputAction::new("Aim", ActionValueType::Bool)
1263                    .with_category("Combat")
1264                    .with_binding(ActionBinding::new(InputBinding::MouseButton(MouseButton::Right), InputDevice::Mouse)
1265                        .with_trigger(InputTrigger::Down))
1266                    .with_binding(ActionBinding::new(InputBinding::GamepadAxis(GamepadAxis::LeftTrigger, 1.0), InputDevice::GamepadXbox)
1267                        .with_modifier(InputModifier::DeadZone { lower: 0.3, upper: 1.0, kind: DeadZoneKind::Axial }))
1268            )
1269            .with_action(
1270                InputAction::new("Reload", ActionValueType::Bool)
1271                    .with_category("Combat")
1272                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::R), InputDevice::Keyboard))
1273                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::West), InputDevice::GamepadXbox))
1274            )
1275            .with_action(
1276                InputAction::new("Interact", ActionValueType::Bool)
1277                    .with_category("Interaction")
1278                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::E), InputDevice::Keyboard))
1279                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::North), InputDevice::GamepadXbox))
1280            )
1281            .with_action(
1282                InputAction::new("Inventory", ActionValueType::Bool)
1283                    .with_category("UI")
1284                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::I), InputDevice::Keyboard))
1285                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::DPadUp), InputDevice::GamepadXbox))
1286            )
1287            .with_action(
1288                InputAction::new("Pause", ActionValueType::Bool)
1289                    .with_category("System")
1290                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Escape), InputDevice::Keyboard))
1291                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::Start), InputDevice::GamepadXbox))
1292            );
1293
1294        let ui_context = ActionMapContext::new("UI", 50)
1295            .with_action(
1296                InputAction::new("Navigate", ActionValueType::Vec2)
1297                    .with_category("UI")
1298                    .with_binding(ActionBinding::analog_binding(InputBinding::GamepadAxis(GamepadAxis::LeftStickY, 1.0), InputDevice::GamepadXbox))
1299            )
1300            .with_action(
1301                InputAction::new("Confirm", ActionValueType::Bool)
1302                    .with_category("UI")
1303                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Return), InputDevice::Keyboard))
1304                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::South), InputDevice::GamepadXbox))
1305            )
1306            .with_action(
1307                InputAction::new("Cancel", ActionValueType::Bool)
1308                    .with_category("UI")
1309                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Escape), InputDevice::Keyboard))
1310                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::East), InputDevice::GamepadXbox))
1311            )
1312            .with_action(
1313                InputAction::new("TabNext", ActionValueType::Bool)
1314                    .with_category("UI")
1315                    .with_binding(ActionBinding::new(InputBinding::Key(KeyCode::Tab), InputDevice::Keyboard))
1316                    .with_binding(ActionBinding::new(InputBinding::GamepadButton(GamepadButton::RightBumper), InputDevice::GamepadXbox))
1317            );
1318
1319        let mut deadzone_configs = HashMap::new();
1320        deadzone_configs.insert(GamepadAxis::LeftStickX, DeadZoneConfig::default());
1321        deadzone_configs.insert(GamepadAxis::LeftStickY, DeadZoneConfig::default());
1322        deadzone_configs.insert(GamepadAxis::RightStickX, DeadZoneConfig { inner_radius: 0.12, ..Default::default() });
1323        deadzone_configs.insert(GamepadAxis::RightStickY, DeadZoneConfig { inner_radius: 0.12, ..Default::default() });
1324
1325        let haptic_effects = vec![
1326            HapticEffect::gun_shot(),
1327            HapticEffect::explosion(),
1328            HapticEffect::footstep(),
1329            HapticEffect::ui_click(),
1330        ];
1331
1332        let sensitivity_profiles = vec![
1333            SensitivityProfile::default(),
1334            SensitivityProfile::esports(),
1335            SensitivityProfile::casual(),
1336            SensitivityProfile::console(),
1337        ];
1338
1339        let combos = vec![
1340            ComboDefinition::new("DashForward", "Dash")
1341                .add_input("Move_Forward", 0.8)
1342                .add_input("Move_Forward", 0.8),
1343            ComboDefinition::new("SpinAttack", "SpinAttack")
1344                .add_input("Fire", 0.5)
1345                .add_input("Fire", 0.5)
1346                .add_input("Fire", 0.5),
1347        ];
1348
1349        Self {
1350            name: "FPS Default".to_string(),
1351            contexts: vec![gameplay, ui_context],
1352            haptic_effects,
1353            macros: Vec::new(),
1354            combos,
1355            sensitivity_profiles,
1356            active_sensitivity_profile: 0,
1357            deadzone_configs,
1358            accessibility: AccessibilitySettings::default(),
1359            gesture_bindings: Vec::new(),
1360            virtual_cursor: VirtualCursor::default(),
1361            replay: InputReplay::default(),
1362        }
1363    }
1364
1365    pub fn detect_conflicts(&self) -> Vec<BindingConflict> {
1366        let mut conflicts = Vec::new();
1367        for (ci, ctx) in self.contexts.iter().enumerate() {
1368            for (ai, action) in ctx.actions.iter().enumerate() {
1369                for (bi, binding) in action.bindings.iter().enumerate() {
1370                    for (ci2, ctx2) in self.contexts.iter().enumerate() {
1371                        if ci2 < ci { continue; }
1372                        for (ai2, action2) in ctx2.actions.iter().enumerate() {
1373                            if ci2 == ci && ai2 <= ai { continue; }
1374                            for binding2 in &action2.bindings {
1375                                if binding.binding == binding2.binding {
1376                                    conflicts.push(BindingConflict {
1377                                        context_a: ctx.name.clone(),
1378                                        action_a: action.name.clone(),
1379                                        context_b: ctx2.name.clone(),
1380                                        action_b: action2.name.clone(),
1381                                        conflicting_binding: binding.binding.label(),
1382                                        severity: ConflictSeverity::Warning,
1383                                    });
1384                                }
1385                            }
1386                        }
1387                    }
1388                }
1389            }
1390        }
1391        conflicts
1392    }
1393
1394    pub fn all_actions(&self) -> Vec<(&str, &InputAction)> {
1395        self.contexts.iter()
1396            .flat_map(|c| c.actions.iter().map(move |a| (c.name.as_str(), a)))
1397            .collect()
1398    }
1399
1400    pub fn actions_by_category(&self) -> HashMap<&str, Vec<&InputAction>> {
1401        let mut map: HashMap<&str, Vec<&InputAction>> = HashMap::new();
1402        for ctx in &self.contexts {
1403            for action in &ctx.actions {
1404                map.entry(action.category.as_str()).or_default().push(action);
1405            }
1406        }
1407        map
1408    }
1409}
1410
1411// ---------------------------------------------------------------------------
1412// Input editor state
1413// ---------------------------------------------------------------------------
1414
1415#[derive(Debug, Clone, Copy, PartialEq)]
1416pub enum InputEditorPanel {
1417    ActionMaps,
1418    Bindings,
1419    Deadzone,
1420    Haptics,
1421    Sensitivity,
1422    Macros,
1423    Combos,
1424    Accessibility,
1425    Devices,
1426    Replay,
1427    Gestures,
1428    Conflicts,
1429}
1430
1431impl InputEditorPanel {
1432    pub fn label(&self) -> &'static str {
1433        match self {
1434            Self::ActionMaps => "Action Maps",
1435            Self::Bindings => "Bindings",
1436            Self::Deadzone => "Dead Zone",
1437            Self::Haptics => "Haptics",
1438            Self::Sensitivity => "Sensitivity",
1439            Self::Macros => "Macros",
1440            Self::Combos => "Combos",
1441            Self::Accessibility => "Accessibility",
1442            Self::Devices => "Devices",
1443            Self::Replay => "Replay",
1444            Self::Gestures => "Gestures",
1445            Self::Conflicts => "Conflicts",
1446        }
1447    }
1448}
1449
1450#[derive(Debug)]
1451pub struct InputEditor {
1452    pub asset: InputAsset,
1453    pub active_panel: InputEditorPanel,
1454    pub selected_context: Option<usize>,
1455    pub selected_action: Option<usize>,
1456    pub selected_binding: Option<usize>,
1457    pub selected_haptic: Option<usize>,
1458    pub selected_macro: Option<usize>,
1459    pub rebinding_mode: bool,
1460    pub rebind_action: Option<String>,
1461    pub rebind_binding_index: Option<usize>,
1462    pub gamepad_state: GamepadState,
1463    pub keyboard_state: KeyboardState,
1464    pub mouse_state: MouseState,
1465    pub live_input_values: HashMap<String, f32>,
1466    pub deadzone_test_input: Vec2,
1467    pub deadzone_test_output: Vec2,
1468    pub selected_deadzone_axis: GamepadAxis,
1469    pub haptic_preview_time: f32,
1470    pub haptic_playing: bool,
1471    pub search_query: String,
1472    pub show_all_platforms: bool,
1473    pub active_device_tab: InputDevice,
1474    pub conflicts: Vec<BindingConflict>,
1475    pub conflict_filter: Option<ConflictSeverity>,
1476    pub history: Vec<InputAsset>,
1477    pub history_pos: usize,
1478}
1479
1480impl Default for InputEditor {
1481    fn default() -> Self {
1482        let asset = InputAsset::default_fps();
1483        let conflicts = asset.detect_conflicts();
1484        Self {
1485            asset,
1486            active_panel: InputEditorPanel::ActionMaps,
1487            selected_context: Some(0),
1488            selected_action: None,
1489            selected_binding: None,
1490            selected_haptic: None,
1491            selected_macro: None,
1492            rebinding_mode: false,
1493            rebind_action: None,
1494            rebind_binding_index: None,
1495            gamepad_state: GamepadState::synthetic_idle(),
1496            keyboard_state: KeyboardState::default(),
1497            mouse_state: MouseState::default(),
1498            live_input_values: HashMap::new(),
1499            deadzone_test_input: Vec2::ZERO,
1500            deadzone_test_output: Vec2::ZERO,
1501            selected_deadzone_axis: GamepadAxis::LeftStickX,
1502            haptic_preview_time: 0.0,
1503            haptic_playing: false,
1504            search_query: String::new(),
1505            show_all_platforms: true,
1506            active_device_tab: InputDevice::Keyboard,
1507            conflicts,
1508            conflict_filter: None,
1509            history: Vec::new(),
1510            history_pos: 0,
1511        }
1512    }
1513}
1514
1515impl InputEditor {
1516    pub fn snapshot(&mut self) {
1517        self.history.truncate(self.history_pos);
1518        self.history.push(self.asset.clone());
1519        self.history_pos = self.history.len();
1520    }
1521
1522    pub fn undo(&mut self) {
1523        if self.history_pos > 1 {
1524            self.history_pos -= 1;
1525            self.asset = self.history[self.history_pos - 1].clone();
1526            self.refresh_conflicts();
1527        }
1528    }
1529
1530    pub fn redo(&mut self) {
1531        if self.history_pos < self.history.len() {
1532            self.asset = self.history[self.history_pos].clone();
1533            self.history_pos += 1;
1534            self.refresh_conflicts();
1535        }
1536    }
1537
1538    pub fn refresh_conflicts(&mut self) {
1539        self.conflicts = self.asset.detect_conflicts();
1540    }
1541
1542    pub fn begin_rebind(&mut self, action: &str, binding_index: usize) {
1543        self.rebinding_mode = true;
1544        self.rebind_action = Some(action.to_string());
1545        self.rebind_binding_index = Some(binding_index);
1546    }
1547
1548    pub fn cancel_rebind(&mut self) {
1549        self.rebinding_mode = false;
1550        self.rebind_action = None;
1551        self.rebind_binding_index = None;
1552    }
1553
1554    pub fn update_deadzone_test(&mut self, raw: Vec2) {
1555        self.deadzone_test_input = raw;
1556        if let Some(config) = self.asset.deadzone_configs.get(&self.selected_deadzone_axis) {
1557            self.deadzone_test_output = config.apply(raw);
1558        }
1559    }
1560
1561    pub fn update_haptic_preview(&mut self, dt: f32) {
1562        if self.haptic_playing {
1563            self.haptic_preview_time += dt;
1564            if let Some(idx) = self.selected_haptic {
1565                if let Some(effect) = self.asset.haptic_effects.get(idx) {
1566                    if self.haptic_preview_time > effect.duration_secs {
1567                        self.haptic_playing = false;
1568                        self.haptic_preview_time = 0.0;
1569                    }
1570                }
1571            }
1572        }
1573    }
1574
1575    pub fn current_haptic_intensity(&self) -> f32 {
1576        if !self.haptic_playing { return 0.0; }
1577        if let Some(idx) = self.selected_haptic {
1578            if let Some(effect) = self.asset.haptic_effects.get(idx) {
1579                return effect.intensity_at_time(self.haptic_preview_time);
1580            }
1581        }
1582        0.0
1583    }
1584
1585    pub fn filtered_actions(&self) -> Vec<(&str, &InputAction)> {
1586        let q = self.search_query.to_lowercase();
1587        self.asset.all_actions().into_iter().filter(|(_, a)| {
1588            q.is_empty() || a.name.to_lowercase().contains(&q) || a.category.to_lowercase().contains(&q)
1589        }).collect()
1590    }
1591
1592    pub fn active_context(&self) -> Option<&ActionMapContext> {
1593        self.selected_context.and_then(|i| self.asset.contexts.get(i))
1594    }
1595
1596    pub fn active_context_mut(&mut self) -> Option<&mut ActionMapContext> {
1597        self.selected_context.and_then(|i| self.asset.contexts.get_mut(i))
1598    }
1599
1600    pub fn generate_code_bindings(&self) -> String {
1601        let mut lines = Vec::new();
1602        lines.push("// Auto-generated input bindings".to_string());
1603        for ctx in &self.asset.contexts {
1604            lines.push(format!("// Context: {} (priority {})", ctx.name, ctx.priority));
1605            for action in &ctx.actions {
1606                lines.push(format!("//   Action: {} ({:?})", action.name, action.value_type));
1607                for binding in &action.bindings {
1608                    lines.push(format!("//     Binding: {} [{}]", binding.binding.label(), binding.device.label()));
1609                }
1610            }
1611        }
1612        lines.join("\n")
1613    }
1614}