Files
prosperon/input.cm
2026-01-23 11:28:27 -06:00

272 lines
6.8 KiB
Plaintext

// 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) {
if (length(_users) == 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
arrfor(array(old_down), action => {
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) {
push(user.paired_devices, canon.device_id)
}
}
}
return user
}
// For explicit pairing: find user paired to this device
for (var 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(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++) {
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]
}
return {
configure: configure,
ingest: ingest,
user: user,
player1() { return _users[0] },
player2() { return _users[1] },
player3() { return _users[2] },
player4() { return _users[3] },
// Re-export for convenience
devices: devices,
backend: backend
}