Files
prosperon/action.cm
2026-01-21 09:05:02 -06:00

337 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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])
})
// Swiperecognizer 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
}