Added gameplay_input manager.
Refactored menus, player, and escape_capsule to utilize new system. Actions defined in Godot project file with default bindings. Player/Vehicles now react to actions and GameplayInput states instead of polling Input directly. They respond to commands via `_handle_gameplay_command` and should not implement _unhandled_input directly. Kept some debugging input handling code in `player.gd` for things like F3/F5/F4/PageUp/PageDown. We may want to move much of this out of `player.gd` into something that handles the gamestate more generally.
This commit is contained in:
+118
-117
@@ -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 ""]
|
||||
|
||||
Reference in New Issue
Block a user