// Prosperon Input System // Engine-driven input with user pairing and possession dispatch var backend = use('input/backends/sdl3') var devices = use('input/devices') var bindings_mod = use('input/bindings') var router_mod = use('input/router') // Default UI-focused action map var default_action_map = { 'ui_up': ['w', 'up', 'gamepad_dpup'], 'ui_down': ['s', 'down', 'gamepad_dpdown'], 'ui_left': ['a', 'left', 'gamepad_dpleft'], 'ui_right': ['d', 'right', 'gamepad_dpright'], 'confirm': ['return', 'space', 'mouse_button_left', 'gamepad_a'], 'cancel': ['escape', 'gamepad_b'], 'menu': ['escape', 'gamepad_start'] } var default_display_names = { 'ui_up': 'UI Up', 'ui_down': 'UI Down', 'ui_left': 'UI Left', 'ui_right': 'UI Right', 'confirm': 'Confirm', 'cancel': 'Cancel', 'menu': 'Menu' } // Module state var _users = [] var _config = { max_users: 1, pairing: 'last_used', emacs: true, gestures: true, action_map: default_action_map, display_names: default_display_names } var _initialized = false var _window_callback = null // Create an input user function create_user(index, config) { var action_map = {} var display_names = {} // Merge defaults with config for (var k in default_action_map) { action_map[k] = array(default_action_map[k]) display_names[k] = default_display_names[k] } if (config.action_map) { for (var k in config.action_map) { var val = config.action_map[k] action_map[k] = is_array(val) ? array(val) : [val] } } if (config.display_names) { for (var k in config.display_names) { display_names[k] = config.display_names[k] } } var user = { index: index, paired_devices: [], active_device: null, bindings: bindings_mod.make(action_map, display_names), router: null, control_stack: [], // Get current device kind get device_kind() { if (!this.active_device) return 'keyboard' return devices.kind(this.active_device) }, // Get current gamepad type get gamepad_type() { if (!this.active_device) return null return devices.gamepad_type(this.active_device) }, // Get action down state get down() { return this.router ? this.router.down : {} }, // Possess an entity (clears stack, sets as sole target) possess: function(entity) { this.control_stack = [entity] }, // Push entity onto control stack push: function(entity) { this.control_stack.push(entity) }, // Pop from control stack pop: function() { if (this.control_stack.length > 1) { return this.control_stack.pop() } return null }, // Get current control target get target() { return this.control_stack.length > 0 ? this.control_stack[this.control_stack.length - 1] : null }, // Dispatch action to current target dispatch: function(action, data) { var target = this.target if (target && target.on_input) { target.on_input(action, data) } }, // Get icon for action using current device get_icon_for_action: function(action) { return this.bindings.get_icon_for_action(action, this.device_kind, this.gamepad_type) }, // Get primary binding for action using current device get_primary_binding: function(action) { return this.bindings.get_primary_binding(action, this.device_kind) } } // Create router user.router = router_mod.make(user, { emacs: config.emacs, gestures: config.gestures, swipe_min_dist: config.swipe_min_dist, swipe_max_time: config.swipe_max_time, pinch_threshold: config.pinch_threshold }) // Load saved bindings user.bindings.load() return user } // Pick user based on pairing policy function pick_user(canon) { if (_users.length == 0) return null // For last_used: always user 0, just update active device if (_config.pairing == 'last_used') { var user = _users[0] // Only switch on button press, not axis/motion if (canon.kind == 'button' && canon.pressed) { if (user.active_device != canon.device_id) { // Release all held actions when switching device var old_down = user.router.down for (var action in old_down) { if (old_down[action]) { user.dispatch(action, { pressed: false, released: true, time: canon.time }) } } user.active_device = canon.device_id if (find(user.paired_devices, canon.device_id) == null) { user.paired_devices.push(canon.device_id) } } } return user } // For explicit pairing: find user paired to this device for (var i = 0; i < _users.length; i++) { if (find(_users[i].paired_devices, canon.device_id) != null) { _users[i].active_device = canon.device_id return _users[i] } } // Unpaired device - could implement join logic here return null } // Configure the input system function configure(opts) { opts = opts || {} _config.max_users = opts.max_users || 1 _config.pairing = opts.pairing || 'last_used' _config.emacs = opts.emacs != false _config.gestures = opts.gestures != false if (opts.action_map) _config.action_map = opts.action_map if (opts.display_names) _config.display_names = opts.display_names if (opts.on_window) _window_callback = opts.on_window // Copy gesture config _config.swipe_min_dist = opts.swipe_min_dist _config.swipe_max_time = opts.swipe_max_time _config.pinch_threshold = opts.pinch_threshold // Create users _users = [] for (var i = 0; i < _config.max_users; i++) { _users.push(create_user(i, _config)) } _initialized = true } // Ingest a raw SDL event function ingest(raw_evt) { if (!_initialized) { configure({}) } // Translate to canonical format var canon = backend.translate(raw_evt) if (!canon) return // Handle window events specially if (canon.kind == 'window') { if (_window_callback) { _window_callback(canon) } return } // Handle device events if (canon.kind == 'device') { if (canon.control == 'connected') { devices.register(canon) } else if (canon.control == 'disconnected') { devices.unregister(canon.device_id) } return } // Register device devices.register(canon) // Pick user and route var user = pick_user(canon) if (user) { user.router.handle(canon) } } // Get user by index function user(index) { return _users[index] } return { configure: configure, ingest: ingest, user: user, get player1() { return _users[0] }, get player2() { return _users[1] }, get player3() { return _users[2] }, get player4() { return _users[3] }, // Re-export for convenience devices: devices, backend: backend }