diff --git a/Scenes/Scripts/character_creation.gd b/Scenes/Scripts/character_creation.gd index d6ac1691..50e0dd9e 100644 --- a/Scenes/Scripts/character_creation.gd +++ b/Scenes/Scripts/character_creation.gd @@ -64,6 +64,7 @@ var culture_buttons: Array[Button] = [] func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + GameplayInput.set_menu_open(self, true) name_input = _find("NameInput") as LineEdit background_list = _find("BackgroundL") as Control if background_list == null: @@ -126,6 +127,20 @@ func _ready() -> void: if backgrounds.size() > 0: _select_background(backgrounds[0]) _set_sex("male") + if sex_male: + sex_male.grab_focus() + + +## Controller cancel follows the existing back command without changing creation data. +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("ui_cancel"): + get_viewport().set_input_as_handled() + _on_back() + + +## Releases menu ownership when entering gameplay or returning to the main menu. +func _exit_tree() -> void: + GameplayInput.set_menu_open(self, false) func _find(n: String) -> Node: diff --git a/Scenes/Scripts/escape_capsule.gd b/Scenes/Scripts/escape_capsule.gd index c7c9b6ae..b6520290 100644 --- a/Scenes/Scripts/escape_capsule.gd +++ b/Scenes/Scripts/escape_capsule.gd @@ -15,7 +15,6 @@ extends CharacterBody3D var player_inside: bool = false var player_ref: CharacterBody3D = null var using_third_person: bool = false -var f_was_down: bool = false var mass: float = 6500.0 var thrust_force: float = 180000.0 @@ -64,11 +63,12 @@ var _hatch_crew_open: bool = false var _hatch_dock_open: bool = false var _ladder_out: bool = false var _cabin_dim: bool = false -var _v_was: bool = false +var _thrust_input: float = 0.0 func _ready() -> void: - _ensure_actions() + GameplayInput.command_requested.connect(_handle_vehicle_command) + GameplayInput.transient_input_cleared.connect(_clear_vehicle_input) if interaction_area: interaction_area.monitoring = true interaction_area.monitorable = true @@ -193,7 +193,7 @@ func _gravity_accel() -> Vector3: func _update_telemetry() -> void: telem_speed = velocity.length() - telem_thrust = Input.is_key_pressed(KEY_SPACE) + telem_thrust = _thrust_input > 0.0 var earth := _earth() if earth: telem_earth_rng = maxf(0.0, global_position.distance_to(earth.global_position) - EARTH_RADIUS) @@ -262,7 +262,7 @@ func _on_body_entered(body: Node3D) -> void: if body.is_in_group("player"): player_ref = body if interact_prompt and not player_inside: - interact_prompt.text = "Press F to Enter" + interact_prompt.text = "[%s] Enter" % GameplayInput.binding_label(&"interact") interact_prompt.visible = true @@ -273,103 +273,61 @@ func _on_body_exited(body: Node3D) -> void: interact_prompt.visible = false -func _unhandled_input(event: InputEvent) -> void: - if not player_inside: +## Handles semantic vehicle commands; proximity permits entry only on foot. +func _handle_vehicle_command(action: StringName, command_context: int) -> void: + if command_context != GameplayInput.context: return - if event.is_action_pressed("ui_cancel"): - if pause_menu and pause_menu.has_method("open_pause") and player_ref: - pause_menu.open_pause(player_ref) - get_viewport().set_input_as_handled() + if action == &"interact": + if player_inside: + exit_capsule() + elif player_ref and interaction_area.overlaps_body(player_ref) and GameplayInput.context == GameplayInput.Context.ON_FOOT: + enter_capsule() return - if _event_toggles(event, "capsule_gear", KEY_G): - _gear_down = not _gear_down - _set_hull("gear_deploy", 1.0 if _gear_down else 0.0) - get_viewport().set_input_as_handled() - elif _event_toggles(event, "capsule_hatch_crew", KEY_H): - _hatch_crew_open = not _hatch_crew_open - _set_hull("hatch_crew_open", 1.0 if _hatch_crew_open else 0.0) - get_viewport().set_input_as_handled() - elif _event_toggles(event, "capsule_hatch_dock", KEY_J): - _hatch_dock_open = not _hatch_dock_open - _set_hull("hatch_dock_open", 1.0 if _hatch_dock_open else 0.0) - get_viewport().set_input_as_handled() - elif _event_toggles(event, "capsule_ladder", KEY_L): - _ladder_out = not _ladder_out - _set_hull("hatch_ladder_deploy", 1.0 if _ladder_out else 0.0) - get_viewport().set_input_as_handled() - elif _event_toggles(event, "capsule_instruments", KEY_I): - _set_hull("instruments_demo", 1.0) - get_viewport().set_input_as_handled() - elif _event_toggles(event, "capsule_cabin_light", KEY_K): - _cabin_dim = not _cabin_dim - _set_hull("cabin_dim", _cabin_dim) - get_viewport().set_input_as_handled() + if not player_inside or GameplayInput.context != GameplayInput.Context.VEHICLE: + return + match action: + &"toggle_view": + toggle_camera() + &"vehicle_toggle_gear": + _gear_down = not _gear_down + _set_hull("gear_deploy", 1.0 if _gear_down else 0.0) + &"vehicle_toggle_crew_hatch": + _hatch_crew_open = not _hatch_crew_open + _set_hull("hatch_crew_open", 1.0 if _hatch_crew_open else 0.0) + &"vehicle_toggle_dock_hatch": + _hatch_dock_open = not _hatch_dock_open + _set_hull("hatch_dock_open", 1.0 if _hatch_dock_open else 0.0) + &"capsule_toggle_ladder": + _ladder_out = not _ladder_out + _set_hull("hatch_ladder_deploy", 1.0 if _ladder_out else 0.0) + &"vehicle_activate_instruments": + _set_hull("instruments_demo", 1.0) + &"vehicle_toggle_cabin_lights": + _cabin_dim = not _cabin_dim + _set_hull("cabin_dim", _cabin_dim) -func _ensure_actions() -> void: - var binds := { - "interact": KEY_F, - "toggle_first_person": KEY_V, - "capsule_gear": KEY_G, - "capsule_hatch_crew": KEY_H, - "capsule_hatch_dock": KEY_J, - "capsule_ladder": KEY_L, - "capsule_instruments": KEY_I, - "capsule_cabin_light": KEY_K, - } - for action in binds.keys(): - if InputMap.has_action(action): - continue - InputMap.add_action(action) - var ev := InputEventKey.new() - ev.physical_keycode = binds[action] - InputMap.action_add_event(action, ev) - - -func _event_toggles(event: InputEvent, action: String, key: Key) -> bool: - if InputMap.has_action(action) and event.is_action_pressed(action): - return true - if event is InputEventKey and event.pressed and not event.echo and event.physical_keycode == key: - return true - return false - - -func _act_pressed(action: String) -> bool: - return InputMap.has_action(action) and Input.is_action_pressed(action) - - -func _act_just(action: String) -> bool: - return InputMap.has_action(action) and Input.is_action_just_pressed(action) +## Clears held cockpit look and visual thrust on menus, handoffs, or focus loss. +func _clear_vehicle_input() -> void: + free_look = false + cockpit_yaw = 0.0 + cockpit_pitch = 0.0 + _thrust_input = 0.0 + if player_inside: + _set_hull("abort_burn", false) + _set_hull("att_pitch", 0.0) + _set_hull("att_yaw", 0.0) + _set_hull("att_roll", 0.0) +## Updates active vehicle input/cameras while retaining the original flight timing. func _process(delta: float) -> void: - var interact_now := _act_just("interact") or _just_key(KEY_F) - if not player_inside and player_ref and interact_now: - f_was_down = true - enter_capsule() - return if not player_inside: - f_was_down = Input.is_physical_key_pressed(KEY_F) + if player_ref and interact_prompt: + interact_prompt.text = "[%s] Enter" % GameplayInput.binding_label(&"interact") _update_crust_exception() return - if interact_now and not f_was_down: - f_was_down = true - exit_capsule() - return - f_was_down = _act_pressed("interact") or Input.is_physical_key_pressed(KEY_F) - if _act_just("toggle_first_person"): - toggle_camera() - elif not InputMap.has_action("toggle_first_person") and _just_key(KEY_V): - toggle_camera() - var was_free = free_look - free_look = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) - if was_free and not free_look and not using_third_person: - cockpit_yaw = 0.0 - cockpit_pitch = 0.0 - if Input.is_key_pressed(KEY_ALT): - Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - else: - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + _update_vehicle_look(delta) handle_flight(delta) _update_telemetry() _update_flight_hud() @@ -383,30 +341,38 @@ func _process(delta: float) -> void: capsule_camera.make_current() -func _just_key(key: Key) -> bool: - var down := Input.is_physical_key_pressed(key) - if key == KEY_V: - var edge := down and not _v_was - _v_was = down - return edge - return false +## Updates cockpit/orbit look without letting menu input reach the vehicle camera. +func _update_vehicle_look(delta: float) -> void: + var cursor_released := GameplayInput.is_held(&"vehicle_release_cursor") + if GameplayInput.gameplay_enabled(): + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if cursor_released else Input.MOUSE_MODE_CAPTURED + var previous_freelook := free_look + free_look = GameplayInput.is_held(&"vehicle_freelook") + if previous_freelook and not free_look and not using_third_person: + cockpit_yaw = 0.0 + cockpit_pitch = 0.0 + var mouse_displacement := GameplayInput.consume_mouse_displacement() + if cursor_released: + return + var mouse_turn := mouse_displacement * look_sensitivity + var controller_turn := GameplayInput.read_controller_look(delta) + if using_third_person: + _apply_vehicle_look(mouse_turn + controller_turn) + else: + _apply_vehicle_look((mouse_turn if free_look else Vector2.ZERO) + controller_turn) -func _input(event: InputEvent) -> void: - if not player_inside: - return - if Input.is_key_pressed(KEY_ALT): - return - if event is InputEventMouseMotion: - if using_third_person: - orbit_yaw -= event.relative.x * look_sensitivity - orbit_pitch += event.relative.y * look_sensitivity - orbit_pitch = clampf(orbit_pitch, orbit_pitch_min, orbit_pitch_max) - elif free_look: - cockpit_yaw -= event.relative.x * look_sensitivity - cockpit_pitch -= event.relative.y * look_sensitivity - cockpit_yaw = clamp(cockpit_yaw, -cockpit_look_limit_yaw, cockpit_look_limit_yaw) - cockpit_pitch = clamp(cockpit_pitch, -cockpit_look_limit_down, cockpit_look_limit_up) +## Applies angular displacement in radians while preserving existing camera limits. +func _apply_vehicle_look(look_delta: Vector2) -> void: + if using_third_person: + orbit_yaw -= look_delta.x + orbit_pitch += look_delta.y + orbit_pitch = clampf(orbit_pitch, orbit_pitch_min, orbit_pitch_max) + else: + cockpit_yaw -= look_delta.x + cockpit_pitch -= look_delta.y + cockpit_yaw = clampf(cockpit_yaw, -cockpit_look_limit_yaw, cockpit_look_limit_yaw) + cockpit_pitch = clampf(cockpit_pitch, -cockpit_look_limit_down, cockpit_look_limit_up) func enter_capsule() -> void: @@ -436,7 +402,6 @@ func enter_capsule() -> void: orbit_yaw = 0.0 orbit_pitch = 0.4 using_third_person = false - f_was_down = true free_look = false if camera_pivot: camera_pivot.top_level = false @@ -454,6 +419,7 @@ func enter_capsule() -> void: if flight_label: flight_label.visible = true _update_crust_exception() + GameplayInput.set_context(GameplayInput.Context.VEHICLE) func exit_capsule() -> void: @@ -463,7 +429,6 @@ func exit_capsule() -> void: player_inside = false velocity = Vector3.ZERO free_look = false - f_was_down = false using_third_person = false _set_hull("abort_burn", false) _set_hull("rcs_direction", 0) @@ -504,6 +469,7 @@ func exit_capsule() -> void: FloatingOrigin.set_focus(exiting_player) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED player_ref = null + GameplayInput.set_context(GameplayInput.Context.ON_FOOT) func toggle_camera() -> void: @@ -569,27 +535,17 @@ func update_third_person_camera() -> void: third_person_camera.global_basis = cam_basis +## Reads signed flight intent while preserving the existing flight equations/timing. func handle_flight(delta: float) -> void: _update_crust_exception() - var input_pitch := 0.0 - var input_roll := 0.0 - var input_yaw := 0.0 - if Input.is_key_pressed(KEY_W): - input_pitch -= 1.0 - if Input.is_key_pressed(KEY_S): - input_pitch += 1.0 - if Input.is_key_pressed(KEY_A): - input_yaw += 1.0 - if Input.is_key_pressed(KEY_D): - input_yaw -= 1.0 - if Input.is_key_pressed(KEY_Q): - input_roll -= 1.0 - if Input.is_key_pressed(KEY_E): - input_roll += 1.0 + var input_pitch := GameplayInput.read_axis(&"vehicle_pitch_up", &"vehicle_pitch_down") + var input_roll := GameplayInput.read_axis(&"vehicle_roll_left", &"vehicle_roll_right") + var input_yaw := GameplayInput.read_axis(&"vehicle_yaw_right", &"vehicle_yaw_left") rotate_object_local(Vector3.RIGHT, input_pitch * rotation_speed * delta) rotate_object_local(Vector3.FORWARD, -input_yaw * rotation_speed * delta) rotate_object_local(Vector3.UP, input_roll * rotation_speed * delta) - var thrusting := Input.is_key_pressed(KEY_SPACE) + _thrust_input = GameplayInput.read_strength(&"vehicle_thrust") + var thrusting := _thrust_input > 0.0 _set_hull("abort_burn", thrusting) _set_hull("att_pitch", input_pitch) _set_hull("att_yaw", input_yaw) @@ -597,7 +553,7 @@ func handle_flight(delta: float) -> void: _set_hull("rcs_direction", 0) var acceleration: Vector3 = _gravity_accel() if thrusting: - acceleration += global_transform.basis.y * (thrust_force / mass) + acceleration += global_transform.basis.y * (thrust_force / mass) * _thrust_input velocity += acceleration * delta var near_surface := is_on_floor() or _crust_exception if not near_surface: diff --git a/Scenes/Scripts/gameplay_input.gd b/Scenes/Scripts/gameplay_input.gd new file mode 100644 index 00000000..664e6d40 --- /dev/null +++ b/Scenes/Scripts/gameplay_input.gd @@ -0,0 +1,287 @@ +extends Node +## Device-independent gameplay input. Bindings remain in Godot's InputMap. + +signal command_requested(action: StringName, command_context: int) +signal transient_input_cleared + +## Gameplay input context enumeration. +## ON_FOOT: Player is on foot and can perform standard movement actions. +## VEHICLE: Player is controlling a vehicle and movement actions are mapped accordingly. +enum Context { ON_FOOT, VEHICLE } + +## Current gameplay input context. +var context: Context = Context.ON_FOOT +## Whether sprint is currently enabled. +var sprint_enabled: bool = false +## Controller turn speed for look input. +var controller_turn_speed: Vector2 = Vector2(2.5, 2.0) +## Whether the Y-axis of the controller look input is inverted. +var invert_controller_y: bool = false +## Deadzone for movement stick input. +var movement_deadzone: float = 0.2 +## Deadzone for look stick input. +var look_deadzone: float = 0.2 +## List of nodes that currently own the menu, blocking gameplay input. +var _menu_owners: Array[Node] = [] +## Accumulated mouse displacement since the last frame. +var _mouse_displacement: Vector2 = Vector2.ZERO +## Whether a jump has been requested this frame. +var _jump_requested: bool = false +## Whether the input system is waiting for all controls to return to neutral. +var _awaiting_neutral: bool = false +## Whether the game window is currently focused. +var _window_focused: bool = true +## Dictionary tracking currently processed but not yet canceled: i.e. when you press the fire button and it has been handled but you have not yet released the button. +var _latched_commands: Dictionary = {} +## Outgoing/incoming controls monitored until an ownership handoff reaches neutral. +var _neutral_actions: Array[StringName] = [] + +## List of all trigger-based actions the player can perform. +const TRIGGER_ACTIONS: Array[StringName] = [ + &"interact", &"combat_reload", &"character_toggle_inventory", &"toggle_view", + &"combat_toggle_weapon", &"character_toggle_mag_boots", &"character_sprint", &"character_jump", &"combat_fire", + &"vehicle_toggle_gear", &"vehicle_toggle_crew_hatch", &"vehicle_toggle_dock_hatch", + &"capsule_toggle_ladder", &"vehicle_activate_instruments", &"vehicle_toggle_cabin_lights", +] +## Explicit command membership; action prefixes are organizational only. +const CONTEXT_TRIGGER_ACTIONS: Dictionary = { + Context.ON_FOOT: [ + &"interact", &"toggle_view", &"character_toggle_inventory", &"character_toggle_mag_boots", + &"character_sprint", &"character_jump", &"combat_fire", &"combat_reload", &"combat_toggle_weapon", + ], + Context.VEHICLE: [ + &"interact", &"toggle_view", &"vehicle_toggle_gear", &"vehicle_toggle_crew_hatch", + &"vehicle_toggle_dock_hatch", &"capsule_toggle_ladder", &"vehicle_activate_instruments", + &"vehicle_toggle_cabin_lights", + ], +} +## Commands requiring the controller auxiliary modifier, independent of their names. +const VEHICLE_AUXILIARY_ACTIONS: Array[StringName] = [ + &"vehicle_toggle_gear", &"vehicle_toggle_crew_hatch", &"vehicle_toggle_dock_hatch", + &"capsule_toggle_ladder", &"vehicle_activate_instruments", &"vehicle_toggle_cabin_lights", +] +## List of all axis-based actions the player can perform. +const AXIS_ACTIONS: Array[StringName] = [ + &"character_move_left", &"character_move_right", &"character_move_forward", &"character_move_back", + &"look_left", &"look_right", &"look_up", &"look_down", + &"eva_roll_left", &"eva_roll_right", &"vehicle_pitch_up", &"vehicle_pitch_down", + &"vehicle_yaw_left", &"vehicle_yaw_right", &"vehicle_roll_left", &"vehicle_roll_right", +] +## Look is shared; character/EVA and vehicle axes belong to distinct contexts. +const SHARED_AXIS_ACTIONS: Array[StringName] = [ + &"look_left", &"look_right", &"look_up", &"look_down", +] +const CONTEXT_AXIS_ACTIONS: Dictionary = { + Context.ON_FOOT: [ + &"character_move_left", &"character_move_right", &"character_move_forward", &"character_move_back", + &"eva_roll_left", &"eva_roll_right", + ], + Context.VEHICLE: [ + &"vehicle_pitch_up", &"vehicle_pitch_down", &"vehicle_yaw_left", &"vehicle_yaw_right", + &"vehicle_roll_left", &"vehicle_roll_right", + ], +} +## Held controls are polled, not dispatched through command_requested. +const CONTEXT_HELD_ACTIONS: Dictionary = { + Context.ON_FOOT: [&"combat_aim", &"freelook", &"release_cursor"], + Context.VEHICLE: [&"vehicle_thrust", &"vehicle_freelook", &"vehicle_auxiliary", &"vehicle_release_cursor"], +} + +## Connects device/focus boundaries; project defaults are never reapplied on spawn. +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + Input.joy_connection_changed.connect(_on_controller_connection_changed) + get_tree().root.focus_entered.connect(_on_focus_entered) + get_tree().root.focus_exited.connect(_on_focus_exited) + + +## Updates neutral gating and ends sprint when the movement stick is released. +func _process(_delta: float) -> void: + for owner_index in range(_menu_owners.size() - 1, -1, -1): + if not is_instance_valid(_menu_owners[owner_index]): + _menu_owners.remove_at(owner_index) + if _awaiting_neutral and _controls_are_neutral(): + _awaiting_neutral = false + _neutral_actions.clear() + if not gameplay_enabled() or read_movement().length_squared() < 0.0001: + sprint_enabled = false + + +## UI receives events first. Only the current gameplay context gets commands. +func _unhandled_input(event: InputEvent) -> void: + for action in _latched_commands.keys(): + if event.is_action_released(action): + _latched_commands.erase(action) + if not gameplay_enabled(): + return + if event is InputEventMouseMotion: + if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + record_mouse_displacement(event.relative) + return + for action in CONTEXT_TRIGGER_ACTIONS[context]: + if event.is_action_pressed(action) and _command_is_available(action, event): + if _latched_commands.has(action): + return + _latched_commands[action] = true + get_viewport().set_input_as_handled() + if action == &"character_sprint": + sprint_enabled = not sprint_enabled + elif action == &"character_jump": + _jump_requested = true + else: + command_requested.emit(action, context) + return + + +## Changes control ownership and discards commands from the previous owner. +func set_context(next_context: Context) -> void: + _monitor_context_for_neutral(context) + context = next_context + clear_transient_input() + + +## Blocks gameplay while an owning menu is open; nested menus are supported. +func set_menu_open(owner_node: Node, is_open: bool) -> void: + if is_open and not _menu_owners.has(owner_node): + _menu_owners.append(owner_node) + elif not is_open: + _menu_owners.erase(owner_node) + clear_transient_input() + + +## Clears held/queued state and requires neutral controls before gameplay resumes. +func clear_transient_input() -> void: + _monitor_context_for_neutral(context) + _mouse_displacement = Vector2.ZERO + _jump_requested = false + _latched_commands.clear() + sprint_enabled = false + _awaiting_neutral = true + transient_input_cleared.emit() + + +## Polling must use this gate: UI event consumption does not block Input state. +## Returns true if the window is focused, the game is not paused, no menus are open, and we are not awaiting neutral input. +func gameplay_enabled() -> bool: + return _window_focused and not get_tree().paused and _menu_owners.is_empty() and not _awaiting_neutral + + +## Checks explicit membership without relying on action naming prefixes. +func is_action_available(action: StringName) -> bool: + return ( + CONTEXT_TRIGGER_ACTIONS[context].has(action) + or SHARED_AXIS_ACTIONS.has(action) + or CONTEXT_AXIS_ACTIONS[context].has(action) + or CONTEXT_HELD_ACTIONS[context].has(action) + ) + + +## Returns body-relative movement, with forward on negative Y and magnitude <= 1. +func read_movement() -> Vector2: + if not gameplay_enabled() or context != Context.ON_FOOT: + return Vector2.ZERO + return Input.get_vector("character_move_left", "character_move_right", "character_move_forward", "character_move_back", movement_deadzone) + + +## Returns signed analog strength without exposing device axes to gameplay. +func read_axis(negative_action: StringName, positive_action: StringName) -> float: + if not gameplay_enabled() or not is_action_available(negative_action) or not is_action_available(positive_action): + return 0.0 + return Input.get_axis(negative_action, positive_action) + + +## Returns an unsigned analog command strength in the range 0..1. +func read_strength(action: StringName) -> float: + return Input.get_action_strength(action) if gameplay_enabled() and is_action_available(action) else 0.0 + + +## Returns a held action; future automatic weapons can use this independently of presses. +func is_held(action: StringName) -> bool: + return gameplay_enabled() and is_action_available(action) and Input.is_action_pressed(action) + + +## Consumes one UI-aware jump request during the next physics update. +func consume_jump_request() -> bool: + var requested: bool = _jump_requested and gameplay_enabled() + _jump_requested = false + return requested + + +## Accumulates mouse pixels independently of the render/physics update rates. +func record_mouse_displacement(displacement: Vector2) -> void: + if gameplay_enabled(): + _mouse_displacement += displacement + + +## Consumes mouse pixels, once per camera update, without time scaling. +func consume_mouse_displacement() -> Vector2: + var displacement := _mouse_displacement if gameplay_enabled() else Vector2.ZERO + _mouse_displacement = Vector2.ZERO + return displacement + + +## Returns stick angular displacement in radians for this frame; positive Y looks down. +func read_controller_look(delta: float) -> Vector2: + if not gameplay_enabled(): + return Vector2.ZERO + var stick := Input.get_vector("look_left", "look_right", "look_up", "look_down", look_deadzone) + if invert_controller_y: + stick.y = -stick.y + return stick * controller_turn_speed * delta + + +## Returns a current-device binding label, with a fallback for unmapped actions. +func binding_label(action: StringName) -> String: + var helper := get_node_or_null("/root/InputHelper") + if helper == null: + return "Unbound" + var binding: InputEvent = helper.get_keyboard_or_joypad_input_for_action(action) + return helper.get_label_for_input(binding) if binding != null else "Unbound" + + +## Restricts contextual commands and routes modifier-layer controller buttons. +func _command_is_available(action: StringName, event: InputEvent) -> bool: + if not CONTEXT_TRIGGER_ACTIONS[context].has(action): + return false + if context == Context.VEHICLE and event is InputEventJoypadButton: + var auxiliary_layer := Input.is_action_pressed("vehicle_auxiliary") + return auxiliary_layer if VEHICLE_AUXILIARY_ACTIONS.has(action) else not auxiliary_layer + return true + + +## Adds only relevant controls to a handoff; unrelated future axes cannot block it. +func _monitor_context_for_neutral(monitored_context: Context) -> void: + var actions: Array = CONTEXT_TRIGGER_ACTIONS[monitored_context] + SHARED_AXIS_ACTIONS + actions.append_array(CONTEXT_AXIS_ACTIONS[monitored_context]) + actions.append_array(CONTEXT_HELD_ACTIONS[monitored_context]) + for action in actions: + if not _neutral_actions.has(action): + _neutral_actions.append(action) + + +## Waits for outgoing/incoming controls, including latched command releases. +func _controls_are_neutral() -> bool: + for action in _neutral_actions: + if not AXIS_ACTIONS.has(action) and Input.is_action_pressed(action): + return false + if Input.get_action_strength(action) > 0.2: + return false + return true + + +## Disconnects clear held commands; reconnects require neutral input too. +func _on_controller_connection_changed(_device: int, _connected: bool) -> void: + clear_transient_input() + + +## Window focus loss must not leave firing, look, or sprint latched. +func _on_focus_exited() -> void: + _window_focused = false + clear_transient_input() + + +## Focus restoration requires a fresh press rather than replaying held controls. +func _on_focus_entered() -> void: + _window_focused = true + clear_transient_input() + diff --git a/Scenes/Scripts/gameplay_input.gd.uid b/Scenes/Scripts/gameplay_input.gd.uid new file mode 100644 index 00000000..1fb903e0 --- /dev/null +++ b/Scenes/Scripts/gameplay_input.gd.uid @@ -0,0 +1 @@ +uid://bd26m8dx3k2pn diff --git a/Scenes/Scripts/main_menu.gd b/Scenes/Scripts/main_menu.gd index 83b90ad2..60a138a9 100644 --- a/Scenes/Scripts/main_menu.gd +++ b/Scenes/Scripts/main_menu.gd @@ -7,6 +7,7 @@ extends Control func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + GameplayInput.set_menu_open(self, true) if new_game_button: new_game_button.pressed.connect(_on_new_game) if continue_button: @@ -15,6 +16,12 @@ func _ready() -> void: quit_button.pressed.connect(_on_quit) if continue_button: continue_button.disabled = not GameState.has_save + new_game_button.grab_focus() + + +## Removes menu ownership when changing scenes so gameplay can resume. +func _exit_tree() -> void: + GameplayInput.set_menu_open(self, false) func _on_new_game() -> void: diff --git a/Scenes/Scripts/pause_menu.gd b/Scenes/Scripts/pause_menu.gd index 3d70c9a8..eab51da6 100644 --- a/Scenes/Scripts/pause_menu.gd +++ b/Scenes/Scripts/pause_menu.gd @@ -17,8 +17,11 @@ func _ready() -> void: quit_button.pressed.connect(_on_quit_to_menu) +## Pause has its own binding; menu cancel must not pause during gameplay. func _input(event: InputEvent) -> void: - if not event.is_action_pressed("ui_cancel"): + var pause_pressed := event.is_action_pressed("ui_pause") + var cancel_pressed := event.is_action_pressed("ui_cancel") + if not pause_pressed and not (visible and cancel_pressed): return var banner := get_node_or_null("/root/Node3D/HUD/ScrambleBanner") if banner and banner.visible: @@ -27,6 +30,8 @@ func _input(event: InputEvent) -> void: _on_resume() get_viewport().set_input_as_handled() return + if not GameplayInput.gameplay_enabled(): + return var p := get_tree().get_first_node_in_group("player") if p == null: p = get_node_or_null("/root/Node3D/Player") @@ -35,16 +40,21 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +## Blocks gameplay and selects a controller-accessible control before pausing. func open_pause(player_ref: Node) -> void: player = player_ref visible = true + GameplayInput.set_menu_open(self, true) + resume_button.grab_focus() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE get_tree().paused = true +## Restores control only after currently held gameplay inputs return to neutral. func _on_resume() -> void: visible = false get_tree().paused = false + GameplayInput.set_menu_open(self, false) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -59,5 +69,6 @@ func _on_save() -> void: func _on_quit_to_menu() -> void: get_tree().paused = false + GameplayInput.set_menu_open(self, false) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE get_tree().change_scene_to_file("res://Scenes/main_menu.tscn") diff --git a/Scenes/Scripts/player.gd b/Scenes/Scripts/player.gd index 3c9e8358..e8044202 100644 --- a/Scenes/Scripts/player.gd +++ b/Scenes/Scripts/player.gd @@ -117,34 +117,14 @@ var interact_cd: float = 0.0 var jump_anim_cd: float = 0.0 var _crust_on: bool = false var _moon_colliders: Array[PhysicsBody3D] = [] - - -func _ensure_action(action: String, keycode: Key) -> void: - if not InputMap.has_action(action): - InputMap.add_action(action) - for ev in InputMap.action_get_events(action): - if ev is InputEventKey and (ev as InputEventKey).physical_keycode == keycode: - return - var evk := InputEventKey.new() - evk.physical_keycode = keycode - InputMap.action_add_event(action, evk) - - -func _ensure_default_actions() -> void: - _ensure_action("interact", KEY_F) - _ensure_action("mag_boots", KEY_C) - _ensure_action("toggle_mag_boots", KEY_C) - _ensure_action("toggle_pistol", KEY_G) - _ensure_action("reload", KEY_R) - _ensure_action("toggle_inventory", KEY_TAB) - _ensure_action("toggle_first_person", KEY_V) - _ensure_action("sprint", KEY_SHIFT) - _ensure_action("eva_roll_left", KEY_Q) - _ensure_action("eva_roll_right", KEY_E) +var _movement_input: Vector2 = Vector2.ZERO +var _fire_requested: bool = false func _ready() -> void: - _ensure_default_actions() + GameplayInput.command_requested.connect(_handle_gameplay_command) + GameplayInput.transient_input_cleared.connect(_clear_player_input) + GameplayInput.set_context(GameplayInput.Context.ON_FOOT) scale = Vector3.ONE safe_margin = 0.05 floor_snap_length = 0.45 @@ -383,6 +363,7 @@ func _find_l5_spawn() -> Node3D: func set_in_vehicle(value: bool) -> void: in_vehicle = value + _clear_player_input() if value: pressurized = true _set_moon_surface_visible(true) @@ -408,7 +389,10 @@ func _update_waist_aim() -> void: look_loader.set_aim_pitch(pitch) +## Updates device-independent camera look before existing visual camera smoothing. func _process(delta: float) -> void: + if not in_vehicle: + _update_player_look(delta) if first_person: _cam_pos_target = _fp_cam_pos() _update_fp_meshes() @@ -723,92 +707,17 @@ func _set_alt_look(on: bool) -> void: camera_pivot.rotation.z = 0.0 +## Keeps development shortcuts separate from remappable gameplay controls. func _unhandled_input(event: InputEvent) -> void: - if in_vehicle: + if in_vehicle or not GameplayInput.gameplay_enabled(): return - if vendor_menu and vendor_menu.visible: - if event.is_action_pressed("ui_cancel"): - close_vendor_menu() - return - if event is InputEventKey and event.keycode == KEY_QUOTELEFT: - if event.pressed: - Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - else: - if not get_tree().paused and not (vendor_menu and vendor_menu.visible): - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED - return - if event is InputEventKey and event.keycode == KEY_ALT: - _set_alt_look(event.pressed) - return - if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: - if pistol_drawn: - ads = event.pressed - _apply_camera_mode(false) - if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed: - if pistol_drawn: - _try_fire() - if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: - if _is_eva(): - # Whole-body look in 1P and 3P EVA. World-Y yaw felt broken after a roll. - rotate_object_local(Vector3.UP, -event.relative.x * mouse_sensitivity) - rotate_object_local(Vector3.RIGHT, -event.relative.y * mouse_sensitivity) - if camera_pivot: - camera_pivot.rotation = Vector3.ZERO - elif first_person: - if free_looking: - # Euler SET — rotate_x+rotate_y on a pitched pivot was the warp. - free_look_yaw -= event.relative.x * mouse_sensitivity - free_look_pitch -= event.relative.y * mouse_sensitivity - free_look_pitch = clamp(free_look_pitch, deg_to_rad(-78), deg_to_rad(62)) - free_look_yaw = clamp(free_look_yaw, deg_to_rad(-75), deg_to_rad(75)) - camera_pivot.rotation = Vector3(free_look_pitch, free_look_yaw, 0.0) - else: - rotate_y(-event.relative.x * mouse_sensitivity) - camera_pivot.rotate_x(-event.relative.y * mouse_sensitivity) - camera_pivot.rotation.x = clamp(camera_pivot.rotation.x, deg_to_rad(-78), deg_to_rad(85)) - elif free_looking: - free_look_yaw -= event.relative.x * free_look_sensitivity - free_look_pitch -= event.relative.y * free_look_sensitivity - free_look_pitch = clamp(free_look_pitch, deg_to_rad(-80), deg_to_rad(80)) - if camera_pivot: - camera_pivot.rotation.y = free_look_yaw - camera_pivot.rotation.x = free_look_pitch - else: - rotate_y(-event.relative.x * mouse_sensitivity) - camera_pivot.rotate_x(-event.relative.y * mouse_sensitivity) - camera_pivot.rotation.x = clamp(camera_pivot.rotation.x, deg_to_rad(-78), deg_to_rad(55)) if event is InputEventKey and event.pressed and not event.echo: match event.keycode: - KEY_1: - _toggle_pistol() KEY_F3: _debug_fp_eye() KEY_F4: if look_loader and look_loader.has_method("cycle_arm_axis"): look_loader.cycle_arm_axis() - if event.is_action_pressed("interact"): - if can_interact and current_interactable: - _try_interact() - if event.is_action_pressed("mag_boots"): - mag_boots_on = not mag_boots_on - if mag_boots_on: - eva_peak_height = 0.0 - velocity = velocity.slide(_walk_up()) * 0.35 - _align_walk(_walk_up()) - _reset_camera_to_walk() - print("Mag Boots: ", "ON" if mag_boots_on else "OFF") - if event.is_action_pressed("toggle_pistol"): - _toggle_pistol() - if event.is_action_pressed("reload"): - _reload_pistol() - if event.is_action_pressed("toggle_inventory"): - inventory_open = not inventory_open - if event.is_action_pressed("toggle_first_person"): - if in_vehicle: - return - first_person = not first_person - _set_alt_look(false) - _apply_camera_mode(false) if event.is_action_pressed("ui_page_up"): food = min(food + 25.0, 100.0) if event.is_action_pressed("ui_page_down"): @@ -825,6 +734,93 @@ func _unhandled_input(event: InputEvent) -> void: banner.hide_banner() +## Dispatches device-independent commands only while the player owns control. +func _handle_gameplay_command(action: StringName, command_context: int) -> void: + if in_vehicle or command_context != GameplayInput.Context.ON_FOOT or GameplayInput.context != command_context: + return + match action: + &"interact": + if can_interact and current_interactable: + _try_interact() + &"combat_fire": + _fire_requested = pistol_drawn + &"combat_reload": + _reload_pistol() + &"combat_toggle_weapon": + _toggle_pistol() + &"character_toggle_inventory": + inventory_open = not inventory_open + &"toggle_view": + first_person = not first_person + _set_alt_look(false) + _apply_camera_mode(false) + &"character_toggle_mag_boots": + _toggle_magnetic_boots() + + +## Toggles boots without changing the existing landing or gravity mechanics. +func _toggle_magnetic_boots() -> void: + mag_boots_on = not mag_boots_on + if mag_boots_on: + eva_peak_height = 0.0 + velocity = velocity.slide(_walk_up()) * 0.35 + _align_walk(_walk_up()) + _reset_camera_to_walk() + print("Mag Boots: ", "ON" if mag_boots_on else "OFF") + + +## Clears pending shots and held camera states at input ownership boundaries. +func _clear_player_input() -> void: + _movement_input = Vector2.ZERO + _fire_requested = false + ads = false + _set_alt_look(false) + _apply_camera_mode(false) + + +## Combines mouse displacement and time-scaled controller turn rate once per frame. +func _update_player_look(delta: float) -> void: + var cursor_released := GameplayInput.is_held(&"release_cursor") + if GameplayInput.gameplay_enabled(): + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if cursor_released else Input.MOUSE_MODE_CAPTURED + var next_freelook := GameplayInput.is_held(&"freelook") + if next_freelook != free_looking: + _set_alt_look(next_freelook) + var next_aim := pistol_drawn and GameplayInput.is_held(&"combat_aim") + if next_aim != ads: + ads = next_aim + _apply_camera_mode(false) + var mouse_displacement := GameplayInput.consume_mouse_displacement() + if cursor_released: + return + var sensitivity := free_look_sensitivity if free_looking and not first_person else mouse_sensitivity + _apply_player_look(mouse_displacement * sensitivity + GameplayInput.read_controller_look(delta)) + + +## Applies radians of yaw/pitch to existing walking, freelook, and body-local EVA modes. +func _apply_player_look(look_delta: Vector2) -> void: + if camera_pivot == null or look_delta == Vector2.ZERO: + return + if _is_eva(): + rotate_object_local(Vector3.UP, -look_delta.x) + rotate_object_local(Vector3.RIGHT, -look_delta.y) + camera_pivot.rotation = Vector3.ZERO + elif free_looking: + free_look_yaw -= look_delta.x + free_look_pitch -= look_delta.y + var minimum_pitch := deg_to_rad(-78.0) if first_person else deg_to_rad(-80.0) + var maximum_pitch := deg_to_rad(62.0) if first_person else deg_to_rad(80.0) + free_look_pitch = clampf(free_look_pitch, minimum_pitch, maximum_pitch) + if first_person: + free_look_yaw = clampf(free_look_yaw, deg_to_rad(-75.0), deg_to_rad(75.0)) + camera_pivot.rotation = Vector3(free_look_pitch, free_look_yaw, 0.0) + else: + rotate_y(-look_delta.x) + var maximum_pitch := deg_to_rad(85.0) if first_person else deg_to_rad(55.0) + camera_pivot.rotation.x = clampf(camera_pivot.rotation.x - look_delta.y, deg_to_rad(-78.0), maximum_pitch) + + +## Samples movement once and consumes queued jump/fire requests on physics ticks. func _physics_process(delta: float) -> void: _update_crust_exception() if fire_cd > 0.0: @@ -838,6 +834,12 @@ func _physics_process(delta: float) -> void: if vendor_menu and vendor_menu.visible: velocity = Vector3.ZERO return + _movement_input = GameplayInput.read_movement() + var jump_requested := GameplayInput.consume_jump_request() + if _fire_requested: + _fire_requested = false + if GameplayInput.gameplay_enabled() and pistol_drawn: + _try_fire() if is_on_floor(): coyote_time = 0.14 else: @@ -846,17 +848,15 @@ func _physics_process(delta: float) -> void: var gvec: Vector3 = _soi_gravity() if _is_eva(): _set_body_visible(true) - if Input.is_action_pressed("eva_roll_left") or Input.is_key_pressed(KEY_Q): - rotate_object_local(Vector3.FORWARD, -2.2 * delta) - if Input.is_action_pressed("eva_roll_right") or Input.is_key_pressed(KEY_E): - rotate_object_local(Vector3.FORWARD, 2.2 * delta) + var roll_input := GameplayInput.read_axis(&"eva_roll_left", &"eva_roll_right") + rotate_object_local(Vector3.FORWARD, roll_input * 2.2 * delta) if gvec.length() > 0.01: velocity += gvec * delta - var input_dir_eva: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") - if input_dir_eva.length() > 0.1: + var input_dir_eva: Vector2 = _movement_input + if input_dir_eva.length() > 0.001: var thrust: Vector3 = ( -transform.basis.z * -input_dir_eva.y + transform.basis.x * input_dir_eva.x - ).normalized() * speed * 0.35 + ) * speed * 0.35 velocity += thrust * delta move_and_slide() _process_needs(delta) @@ -876,13 +876,13 @@ func _physics_process(delta: float) -> void: else: g_amt = maxf(g_amt, MOON_G) velocity += -up * g_amt * delta - var input_dir: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var input_dir: Vector2 = _movement_input var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0.0, input_dir.y)).slide(up) if direction.length() > 0.001: - direction = direction.normalized() + direction = direction.normalized() * input_dir.length() else: direction = Vector3.ZERO - var want_sprint: bool = Input.is_action_pressed("sprint") and input_dir.y < -0.12 + var want_sprint: bool = GameplayInput.sprint_enabled and input_dir.y < -0.12 var current_speed: float = sprint_speed if want_sprint else speed if ads and pistol_drawn: current_speed *= ads_sprint_scale if want_sprint else ads_walk_scale @@ -904,13 +904,13 @@ func _physics_process(delta: float) -> void: velocity = v_h + up * v_up # Jump LAST so floor rebuild cannot eat or replace the impulse. var can_jump: bool = is_on_floor() or coyote_time > 0.0 - if Input.is_action_just_pressed("ui_accept") and can_jump: + if jump_requested and can_jump: coyote_time = 0.0 jump_anim_cd = 0.45 var jv: float = 1.7 if mag_boots_on else 6.4 velocity = velocity.slide(up) + up * jv print("JUMP boots=", mag_boots_on, " jv=", jv, " g=", g_amt, " floor=", is_on_floor()) - if is_on_floor() and not Input.is_action_just_pressed("ui_accept"): + if is_on_floor() and not jump_requested: _try_step(up) move_and_slide() _keep_walk_upright() @@ -1033,6 +1033,7 @@ func open_vendor_menu() -> void: func close_vendor_menu() -> void: if vendor_menu: vendor_menu.visible = false + GameplayInput.set_menu_open(vendor_menu, false) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -1055,10 +1056,10 @@ func _update_animations() -> void: _play_move_anim("Idle") return # Stick, not velocity — accel must not pick the clip. - var stick: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") + var stick: Vector2 = _movement_input var strafe: float = clampf(stick.x, -1.0, 1.0) var back: float = clampf(stick.y, -1.0, 1.0) - var want_sprint: bool = Input.is_action_pressed("sprint") and stick.y < -0.12 + var want_sprint: bool = GameplayInput.sprint_enabled and stick.y < -0.12 var run_amt: float = 1.0 if want_sprint else 0.0 if look_loader and look_loader.has_method("set_loco"): look_loader.set_loco(strafe, back, run_amt, dt, _is_aiming()) @@ -1073,7 +1074,7 @@ func _update_animations() -> void: func _update_hud() -> void: if needs_label: - var interact_text: String = " [F] Use" if can_interact else "" + var interact_text: String = " [%s] Use" % GameplayInput.binding_label(&"interact") if can_interact else "" var gun_text: String = "" if pistol_drawn: gun_text = " | PISTOL %d/%d%s" % [pistol_ammo, pistol_reserve, " ADS" if ads else ""] diff --git a/Scenes/Scripts/vendor_menu.gd b/Scenes/Scripts/vendor_menu.gd index add1ec9c..f513c13d 100644 --- a/Scenes/Scripts/vendor_menu.gd +++ b/Scenes/Scripts/vendor_menu.gd @@ -71,10 +71,14 @@ func _ready() -> void: visible = false +## Claims menu input and gives controllers a predictable initial focus. func open_menu(player_ref: Node) -> void: player = player_ref visible = true + GameplayInput.set_menu_open(self, true) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _update_button_states() + close_button.grab_focus() print("Vendor menu opened") func _update_button_states() -> void: @@ -112,6 +116,15 @@ func _buy(item_name: String, price: int) -> void: else: print("Not enough credits") +## Cancels the vendor without allowing the same event to reach gameplay. +func _unhandled_input(event: InputEvent) -> void: + if visible and event.is_action_pressed("ui_cancel"): + _on_close_pressed() + get_viewport().set_input_as_handled() + + +## Releases menu ownership; the adapter clears queued and held commands. func _on_close_pressed() -> void: visible = false + GameplayInput.set_menu_open(self, false) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED diff --git a/project.godot b/project.godot index 6388164e..d3134dd7 100644 --- a/project.godot +++ b/project.godot @@ -21,6 +21,7 @@ GameState="*uid://b3o7ua8cv55tq" FloatingOrigin="*uid://ckl5inm8ugypo" InputHelper="*uid://cholww48njaeh" Audio="*uid://cujwm7u5pvogl" +GameplayInput="*res://Scenes/Scripts/gameplay_input.gd" [display] @@ -38,6 +39,20 @@ import/blender/enabled=false [input] +ui_accept={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +ui_cancel={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} ui_left={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) @@ -70,49 +85,237 @@ ui_down={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) ] } -toggle_mag_boots={ +character_toggle_mag_boots={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null) ] } interact={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null) ] } -sprint={ +character_sprint={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":7,"pressure":0.0,"pressed":false,"script":null) ] } -toggle_inventory={ +character_toggle_inventory={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":4,"pressure":0.0,"pressed":false,"script":null) ] } -toggle_first_person={ +toggle_view={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":118,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null) ] } -toggle_pistol={ +combat_toggle_weapon={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null) ] } -reload={ +combat_reload={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) ] } eva_roll_left={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":false,"script":null) ] } eva_roll_right={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null) +] +} +character_move_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +] +} +character_move_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +] +} +character_move_forward={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +] +} +character_move_back={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +] +} +look_left={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null) +] +} +look_right={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":1.0,"script":null) +] +} +look_up={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":-1.0,"script":null) +] +} +look_down={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":1.0,"script":null) +] +} +character_jump={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +combat_aim={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":4,"axis_value":1.0,"script":null) +] +} +combat_fire={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null) +] +} +freelook={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194328,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":8,"pressure":0.0,"pressed":false,"script":null) +] +} +release_cursor={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":96,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +ui_pause={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_pitch_up={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +] +} +vehicle_pitch_down={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +] +} +vehicle_yaw_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +] +} +vehicle_yaw_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +] +} +vehicle_roll_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_roll_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_thrust={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null) +] +} +vehicle_freelook={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":8,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_release_cursor={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194328,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +vehicle_auxiliary={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":4,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_toggle_gear={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_toggle_crew_hatch={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_toggle_dock_hatch={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null) +] +} +capsule_toggle_ladder={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_activate_instruments={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null) +] +} +vehicle_toggle_cabin_lights={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null) ] }