// Action mapping system that sits at the root of the scene tree // Consumes raw input and reissues as named actions var io = use('cellfs') var input = use('input') var json = use('json') var action = {} var controller_map = { ps3: 'playstation', ps4: 'playstation', ps5: 'playstation', xbox: 'xbox360', xbox: 'xboxone', switch: 'switchpro', xbox: 'standard', gamecube: 'gamecube', switch: 'joyconleft', switch: 'joyconright', switch: 'joyconpair' } action.get_icon_for_action = function(action) { var bindings = this.get_bindings_for_device(action) if (!length(bindings)) return null var primary_binding = bindings[0] if (this.current_device == 'keyboard') { if (starts_with(primary_binding, 'mouse_button_')) { var button = replace(primary_binding, 'mouse_button_', '') return 'ui/mouse/mouse_' + button + '.png' } else { // Handle special keyboard keys var key_mapping = { 'escape': 'escape', 'return': 'return', 'space': 'space', 'up': 'arrow_up', 'down': 'arrow_down', 'left': 'arrow_left', 'right': 'arrow_right' } var key = key_mapping[primary_binding] || primary_binding return 'ui/keyboard/keyboard_' + key + '.png' } } if (this.current_device == 'gamepad' && this.current_gamepad_type) { var controller_prefix = controller_map[this.current_gamepad_type] || 'playstation' // Map gamepad inputs to icon names var gamepad_mapping = { 'gamepad_a': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_cross' : 'xbox_button_a', 'gamepad_b': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_circle' : 'xbox_button_b', 'gamepad_x': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_square' : 'xbox_button_x', 'gamepad_y': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_triangle' : 'xbox_button_y', 'gamepad_dpup': controller_prefix + '_dpad_up', 'gamepad_dpdown': controller_prefix + '_dpad_down', 'gamepad_dpleft': controller_prefix + '_dpad_left', 'gamepad_dpright': controller_prefix + '_dpad_right', 'gamepad_l1': controller_prefix + '_trigger_l1', 'gamepad_r1': controller_prefix + '_trigger_r1', 'gamepad_l2': controller_prefix + '_trigger_l2', 'gamepad_r2': controller_prefix + '_trigger_r2', 'gamepad_start': this.get_start_button_icon() } var icon_name = gamepad_mapping[primary_binding] if (icon_name) { return 'ui/' + controller_prefix + '/' + icon_name + '.png' } } return null } action.get_start_button_icon = function() { if (this.current_gamepad_type == 'ps3') { return 'playstation3_button_start' } else if (this.current_gamepad_type == 'ps4') { return 'playstation4_button_options' } else if (this.current_gamepad_type == 'ps5') { return 'playstation5_button_options' } else { return 'xbox_button_start' } } var default_action_map = { 'move_up': ['w', 'gamepad_dpup', 'swipe_up'], 'move_left': ['a', 'gamepad_dpleft', 'swipe_left'], 'move_down': ['s', 'gamepad_dpdown', 'swipe_down'], 'move_right': ['d', 'gamepad_dpright', 'swipe_right'], 'accio': ['space', 'gamepad_x'], 'reset': ['r', 'gamepad_y'], 'undo': ['z'], 'menu': ['escape', 'gamepad_start'], '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_b'], 'cancel': ['escape', 'gamepad_a'], // Editor controls 'brush_left': ['gamepad_dpleft', 'left'], 'brush_right': ['gamepad_dpright', 'right'], 'tool_up': ['gamepad_dpup', 'up'], 'tool_down': ['gamepad_dpdown', 'down'], 'tool_use': ['gamepad_a', 'mouse_button_left'], 'editor_undo': [',', 'gamepad_l1'], 'editor_redo': ['.', 'gamepad_r1'], 'editor_inspect': ['C-p'], 'editor_marquee': ['mouse_button_right', 'gamepad_r2'], 'editor_erase': ['x', 'gamepad_b'], 'editor_eyedrop': ['i', 'gamepad_x'], 'editor_grab': ['mouse_button_middle', 'gamepad_y'], 'editor_ccw': ['gamepad_l1', 'q'], 'editor_cw': ['gamepad_r1', 'e'], } default_action_map.witch_up = 'up' default_action_map.witch_down = 'down' default_action_map.witch_left = 'left' default_action_map.witch_right = 'right' default_action_map.accion = 'y' // Utility to detect device from input id function detect_device(input_id) { if (starts_with(input_id, 'gamepad_')) return 'gamepad' if (starts_with(input_id, 'swipe_')) return 'touch' if (starts_with(input_id, 'mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller if (starts_with(input_id, 'touch_')) return 'touch' return 'keyboard' } // Display names for actions var action_display_names = { 'move_up': 'Move Up', 'move_down': 'Move Down', 'move_left': 'Move Left', 'move_right': 'Move Right', 'ui_up': 'UI Up', 'ui_down': 'UI Down', 'ui_left': 'UI Left', 'ui_right': 'UI Right', 'menu': 'Menu', 'confirm': 'Confirm', 'cancel': 'Cancel', 'reset': 'Reset', 'accio': 'Summon', 'brush_left': 'Previous Entity', 'brush_right': 'Next Entity', 'tool_up': 'Previous Tool', 'tool_down': 'Next Tool', 'tool_use': 'Use Tool' } action.down = {} action.action_map = {} action.display_names = action_display_names action.is_rebinding = false action.rebind_target = null action.current_device = 'keyboard' action.current_gamepad_type = null // Copy defaults arrfor(array(default_action_map), function(key) { action.action_map[key] = array(default_action_map[key]) }) // Swipe‐recognizer state & tuning var swipe = { x0:null, y0:null, t0:0 } var SWIPE_MIN_DIST = 30 // pixels var SWIPE_MAX_TIME = 500 // ms action.on_input = function(action_id, evt) { // 1) Detect & store which device user is on (only from raw input, ignore passive inputs) if (action_id != 'mouse_move' && !starts_with(action_id, 'mouse_pos') && evt.pressed) { var new_device = detect_device(action_id) // For keyboard/mouse detection, also check if the event has modifier keys or is a mouse button // This helps distinguish real keyboard/mouse events from mapped actions var is_real_kb_mouse = (new_device == 'keyboard' && (evt.ctrl != null || evt.shift != null || evt.alt != null || starts_with(action_id, 'mouse_button'))) if (new_device == 'keyboard' && !is_real_kb_mouse) { // This might be a mapped action, not a real keyboard/mouse event return } if (new_device != this.current_device) { if (new_device == 'gamepad') { var gamepad_type = evt.which != null ? input.gamepad_id_to_type(evt.which) : 'unknown' log.console("Input switched to 'gamepad' (event: " + action_id + ", type: " + gamepad_type + ")") this.current_gamepad_type = gamepad_type } else if (this.current_device == 'gamepad') { log.console("Input switched to 'keyboard/mouse' (event: " + action_id + ")") } this.current_device = new_device } } // 2) If we're in rebind mode, grab the raw input if (this.is_rebinding) { if (evt.pressed) { // Only bind if it's from the same device type if (detect_device(action_id) == this.current_device) { this.rebind_action(this.rebind_target, action_id) this.save_bindings() } // Exit rebind mode this.is_rebinding = false this.rebind_target = null } return // Don't also fire the mapped action } // 3) Otherwise, find all mapped actions for this input var matched_actions = [] arrfor(array(this.action_map), mapped_action => { if (find(this.action_map[mapped_action], action_id) != null) { push(matched_actions, mapped_action) if (evt.pressed) this.down[mapped_action] = true else if (evt.released) this.down[mapped_action] = false } }) // Send all matched actions (only if we found mappings - this means it's a raw input) if (length(matched_actions) > 0) { for (var i = 0; i < length(matched_actions); i++) { // scene.recurse(game.root, 'on_input', [matched_actions[i], evt]) } } } action.start_rebind = function(action_name) { if (!this.action_map[action_name]) return this.is_rebinding = true this.rebind_target = action_name } action.rebind_action = function(action_name, new_key) { if (!this.action_map[action_name]) return // Remove this key from all other actions arrfor(array(this.action_map), act => { var idx = find(this.action_map[act], new_key) if (idx != null) this.action_map[act] = array(array(this.action_map[act], 0, idx), array(this.action_map[act], idx+1)) }) // Clear existing bindings for the current device from the target action var target_bindings = this.action_map[action_name] for (var i = length(target_bindings) - 1; i >= 0; i--) { if (detect_device(target_bindings[i]) == this.current_device) this.action_map[action_name] = array(array(this.action_map[action_name], 0, i), array(this.action_map[action_name], i+1)) } // Only insert into the target if it's the right device if (detect_device(new_key) == this.current_device) this.action_map[action_name].unshift(new_key) } // Returns bindings for the current device only action.get_bindings_for_device = function(action_name) { var all = this.action_map[action_name] || [] var self = this return filter(all, function(id) { return detect_device(id) == self.current_device }) } // Returns the primary binding for display - prefer keyboard/mouse, then others action.get_primary_binding = function(action_name) { var all = this.action_map[action_name] || [] if (!length(all)) return '(unbound)' // Prefer keyboard/mouse bindings for display stability (mouse buttons now detect as keyboard) var keyboard = filter(all, function(id) { return detect_device(id) == 'keyboard' }) if (length(keyboard)) return keyboard[0] // Fall back to any binding return all[0] } // Returns the binding for the current device, or "Unbound!" if none action.get_current_device_binding = function(action_name) { var device_bindings = this.get_bindings_for_device(action_name) if (!length(device_bindings)) return 'Unbound!' return device_bindings[0] } action.save_bindings = function() { try { io.slurpwrite('keybindings.json', json.encode(this.action_map)) } catch(e) { log.console("Failed to save key bindings:", e) } } action.load_bindings = function() { try { if (io.exists('keybindings.json')) { var data = io.slurp('keybindings.json') var bindings = object(json.decode(data), this.action_map) } } catch(e) { log.console("Failed to load key bindings:", e) } } action.reset_to_defaults = function() { this.action_map = object(default_action_map) this.save_bindings() } return function() { var obj = meme(action) obj.action_map = object(default_action_map) obj.display_names = action_display_names obj.is_rebinding = false obj.rebind_target = null obj.current_device = 'keyboard' obj.current_gamepad_type = null obj.load_bindings() return obj }