// 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 arrfor(array(default_action_map), function(k) { action_map[k] = array(default_action_map[k]) display_names[k] = default_display_names[k] }) if (config.action_map) { arrfor(array(config.action_map), function(k) { var val = config.action_map[k] action_map[k] = is_array(val) ? array(val) : [val] }) } if (config.display_names) { arrfor(array(config.display_names), k => 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 device_kind() { if (!this.active_device) return 'keyboard' return devices.kind(this.active_device) }, // Get current gamepad type gamepad_type() { if (!this.active_device) return null return devices.gamepad_type(this.active_device) }, // Get action down state 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) { push(this.control_stack, entity) }, // Pop from control stack pop: function() { if (length(this.control_stack) > 1) { return pop(this.control_stack) } return null }, // Get current control target target() { return length(this.control_stack) > 0 ? this.control_stack[length(this.control_stack) - 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) { var picked = null var old_down = null var i = 0 if (length(_users) == 0) return null // For last_used: always user 0, just update active device if (_config.pairing == 'last_used') { picked = _users[0] // Only switch on button press, not axis/motion if (canon.kind == 'button' && canon.pressed) { if (picked.active_device != canon.device_id) { // Release all held actions when switching device old_down = picked.router.down() arrfor(array(old_down), action => { if (old_down[action]) { picked.dispatch(action, { pressed: false, released: true, time: canon.time }) } }) picked.active_device = canon.device_id if (find(picked.paired_devices, canon.device_id) == null) { push(picked.paired_devices, canon.device_id) } } } return picked } // For explicit pairing: find user paired to this device for (i = 0; i < length(_users); 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(o) { var opts = o || {} var i = 0 _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 (i = 0; i < _config.max_users; i++) { push(_users, 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] } function snapshot() { var users = [] var i = 0 var u = null var target = null for (i = 0; i < length(_users); i++) { u = _users[i] target = u.target() push(users, { index: u.index, device_kind: u.device_kind(), active_device: u.active_device, paired_devices: array(u.paired_devices), down: u.down(), control_stack_depth: length(u.control_stack), target: target ? (target.name || '(entity)') : null }) } return { max_users: _config.max_users, pairing: _config.pairing, action_map: _config.action_map, users: users } } return { configure: configure, ingest: ingest, user: user, snapshot: snapshot, player1() { return _users[0] }, player2() { return _users[1] }, player3() { return _users[2] }, player4() { return _users[3] }, // Re-export for convenience devices: devices, backend: backend }