352 lines
12 KiB
Plaintext
352 lines
12 KiB
Plaintext
// 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
|
||
for (var key in default_action_map) {
|
||
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 = []
|
||
for (var mapped_action in this.action_map) {
|
||
if (find(this.action_map[mapped_action], action_id) != null) {
|
||
matched_actions.push(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
|
||
for (var act in this.action_map) {
|
||
var idx = find(this.action_map[act], new_key)
|
||
if (idx != null)
|
||
this.action_map[act].splice(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)
|
||
target_bindings.splice(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 {
|
||
var bindings_data = {}
|
||
for (var key in this.action_map) {
|
||
bindings_data[key] = this.action_map[key]
|
||
}
|
||
io.slurpwrite('keybindings.json', json.encode(bindings_data))
|
||
} 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 = json.decode(data)
|
||
for (var key in bindings) {
|
||
if (this.action_map[key]) {
|
||
this.action_map[key] = bindings[key]
|
||
}
|
||
}
|
||
}
|
||
} catch(e) {
|
||
log.console("Failed to load key bindings:", e)
|
||
}
|
||
}
|
||
|
||
action.reset_to_defaults = function() {
|
||
for (var key in default_action_map) {
|
||
this.action_map[key] = array(default_action_map[key])
|
||
}
|
||
this.save_bindings()
|
||
}
|
||
|
||
return function()
|
||
{
|
||
var obj = meme(action)
|
||
obj.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
|
||
// Copy defaults
|
||
for (var key in default_action_map) {
|
||
obj.action_map[key] = array(default_action_map[key])
|
||
}
|
||
obj.load_bindings()
|
||
return obj
|
||
}
|