mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
076d27a564
Playing with a gamepad did not work: all six move_* actions had no joypad event at all, so a pad could yaw/pitch/roll/turbo but could not translate. Nothing caught it because every action existed and the game booted fine — no assertion checked that an action is reachable on *both* devices. Controller layout, on the 6DOF convention (left stick aims, right stick translates), using all six of the pad's analog axes for the ship's six degrees of freedom: left stick yaw + pitch right stick strafe + vertical LB / RB roll RT / LT forward / back L3 turbo R3 ball camera Input is now read with Input.get_axis instead of is_action_pressed, so triggers and sticks are proportional. Keyboard values are unchanged. Three rotation bugs found by measuring a real Ship rather than reading the code: - apply_torque() is world-space and the torque was never rotated into the hull's frame (unlike thrust, which uses -ship_basis.z). Roll input became pitch after a 90 degree turn and inverted at 180, so the controls were correct flying up-field and backwards flying back. - ship.tscn's inertia is Vector3(7, 1, 7) but a flat torque was applied to every axis, giving yaw 7x the angular acceleration of pitch and roll (172 deg/s vs 52). Torque is now scaled per-axis by inertia, so rotation_acceleration means rad/s^2 and all three axes match. Yaw is unchanged. - pitch_down pitched the nose UP: get_axis's arguments were reversed, so the I/K keys and the stick each did the opposite of their label. Menus were unusable on a pad for a separate reason: Godot 4.7 gives ui_up/down/left/right joypad events by default but leaves ui_accept and ui_cancel with none (verified against a pristine project), so a controller could move the highlight and never press anything. A confirms and B goes back. Gameplay exits on a new leave_gameplay action (Escape / Start) rather than ui_cancel, so carrying B for menus cannot abandon a live match. Bindings for both devices are rebindable in Settings -> Controls, persisted to user://input.cfg — a separate file from settings.cfg because VideoSettings.save() rewrites that file wholesale and would drop any section it does not know about. project.godot stays the source of truth for defaults; overrides are only ever a delta on top of a boot-time snapshot. Verified: 268 unit tests, the ENet integration gate, and a 16-sample before/after comparison of networked prediction residuals showing the physics change does not regress them (median 0.083m -> 0.065m). Note for follow-up: every policy in Game/bots/ was trained against the old sluggish, world-axis rotation and will over-rotate until retrained.
451 lines
20 KiB
GDScript
451 lines
20 KiB
GDScript
extends "res://tests/test_case.gd"
|
|
|
|
# Guards the input map and the InputSettings remap layer.
|
|
#
|
|
# The defect this file exists for: project.godot bound joypad events to only 5
|
|
# of the 13 flight actions, so a controller could yaw/pitch/roll/turbo but could
|
|
# not translate at all. Nothing failed, because nothing asserted that a *pair*
|
|
# of bindings exists — the actions were all present and the game booted fine.
|
|
# test_every_action_has_both_a_keyboard_and_a_joypad_binding is that assertion,
|
|
# and it can tell "bound on both devices" from "bound on one", which is the
|
|
# distinction that was actually missing.
|
|
#
|
|
# Several of these tests write to the global InputMap through InputSettings, so
|
|
# each one must restore it before returning or it corrupts every later case in
|
|
# the run (the runner shares one process). reset_all() is the restore.
|
|
|
|
const CONTROLLER_ONLY_ACTIONS := ["toggle_ball_cam", "reset_ball"]
|
|
|
|
|
|
func _joypad_events(action: String) -> Array:
|
|
var out := []
|
|
for event in InputMap.action_get_events(action):
|
|
if InputSettings.device_kind_of(event) == InputSettings.DEVICE_JOYPAD:
|
|
out.append(event)
|
|
return out
|
|
|
|
|
|
func _keyboard_events(action: String) -> Array:
|
|
var out := []
|
|
for event in InputMap.action_get_events(action):
|
|
if InputSettings.device_kind_of(event) == InputSettings.DEVICE_KEYBOARD:
|
|
out.append(event)
|
|
return out
|
|
|
|
|
|
func test_every_rebindable_action_exists() -> void:
|
|
for entry in InputSettings.ACTIONS:
|
|
assert_true(InputMap.has_action(entry["action"]), "InputMap has action %s" % entry["action"])
|
|
|
|
|
|
func test_every_action_has_both_a_keyboard_and_a_joypad_binding() -> void:
|
|
# The regression itself: a controller player must be able to reach every
|
|
# action without touching the keyboard, and vice versa.
|
|
for entry in InputSettings.ACTIONS:
|
|
var action: String = entry["action"]
|
|
assert_true(not _keyboard_events(action).is_empty(), "%s has a keyboard binding" % action)
|
|
assert_true(not _joypad_events(action).is_empty(), "%s has a joypad binding" % action)
|
|
|
|
|
|
func test_apply_is_lossless_against_the_project_defaults() -> void:
|
|
# InputSettings stores one binding per device per action, so apply()
|
|
# rewrites each action's event list to exactly [keyboard, joypad]. If
|
|
# project.godot ever gains a second keyboard event for a rebindable action,
|
|
# booting the game would silently drop it — the action would still work, on
|
|
# fewer keys than the file says. Asserting apply() is a no-op over the
|
|
# defaults is what distinguishes "bindings intact" from "bindings quietly
|
|
# trimmed", which counting events cannot do.
|
|
InputSettings.reset_all()
|
|
var before := {}
|
|
for entry in InputSettings.ACTIONS:
|
|
before[entry["action"]] = InputMap.action_get_events(entry["action"]).size()
|
|
|
|
InputSettings.apply()
|
|
|
|
for entry in InputSettings.ACTIONS:
|
|
var action: String = entry["action"]
|
|
assert_eq(
|
|
InputMap.action_get_events(action).size(),
|
|
before[action],
|
|
"%s keeps every event across apply()" % action
|
|
)
|
|
assert_eq(before[action], 2, "%s has exactly one keyboard and one joypad event" % action)
|
|
|
|
|
|
func test_no_two_actions_share_a_joypad_binding() -> void:
|
|
# Two flight actions sharing one input is silently unplayable rather than an
|
|
# error, and it is easy to reintroduce: an early draft of this layout had A
|
|
# as both turbo and thrust-up, and B as both thrust-down and ui_cancel.
|
|
for entry in InputSettings.ACTIONS:
|
|
var action: String = entry["action"]
|
|
var bound := InputSettings.get_binding(action, InputSettings.DEVICE_JOYPAD)
|
|
assert_true(bound != null, "%s resolves a joypad binding" % action)
|
|
if bound == null:
|
|
continue
|
|
var conflicts := InputSettings.find_conflicts(bound, action)
|
|
assert_true(
|
|
conflicts.is_empty(),
|
|
"%s's joypad binding is unique (also on: %s)" % [action, ", ".join(conflicts)]
|
|
)
|
|
|
|
|
|
func test_no_two_actions_share_a_keyboard_binding() -> void:
|
|
for entry in InputSettings.ACTIONS:
|
|
var action: String = entry["action"]
|
|
var bound := InputSettings.get_binding(action, InputSettings.DEVICE_KEYBOARD)
|
|
assert_true(bound != null, "%s resolves a keyboard binding" % action)
|
|
if bound == null:
|
|
continue
|
|
var conflicts := InputSettings.find_conflicts(bound, action)
|
|
assert_true(
|
|
conflicts.is_empty(),
|
|
"%s's keyboard binding is unique (also on: %s)" % [action, ", ".join(conflicts)]
|
|
)
|
|
|
|
|
|
func _joypad_buttons(action: String) -> Array:
|
|
var out := []
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventJoypadButton:
|
|
out.append(event.button_index)
|
|
return out
|
|
|
|
|
|
func test_menus_are_usable_with_a_controller() -> void:
|
|
# Godot 4.7 ships ui_up/down/left/right with D-pad and stick events but
|
|
# gives ui_accept and ui_cancel NO joypad binding at all (verified against a
|
|
# pristine project). A controller could therefore move the highlight around
|
|
# the main menu and never press anything — the menu looked responsive, which
|
|
# is exactly why it went unnoticed. project.godot binds them explicitly.
|
|
assert_true(JOY_BUTTON_A in _joypad_buttons("ui_accept"), "A confirms in menus")
|
|
assert_true(JOY_BUTTON_B in _joypad_buttons("ui_cancel"), "B goes back in menus")
|
|
# Navigation is the engine default, but assert it so a future override of
|
|
# these actions cannot silently strand a controller player again.
|
|
for action in ["ui_up", "ui_down", "ui_left", "ui_right"]:
|
|
var pad := 0
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
|
pad += 1
|
|
assert_true(pad > 0, "%s is reachable on a controller" % action)
|
|
|
|
|
|
func test_leaving_gameplay_is_not_on_a_face_button() -> void:
|
|
# game_mode.gd exits to the main menu on leave_gameplay, deliberately NOT on
|
|
# ui_cancel: ui_cancel carries B so menus behave conventionally, and B is far
|
|
# too easy to hit by accident to also mean "abandon this match". The two
|
|
# actions being distinct is the whole point, so assert they really differ.
|
|
assert_true(InputMap.has_action("leave_gameplay"), "leave_gameplay exists")
|
|
var buttons := _joypad_buttons("leave_gameplay")
|
|
assert_true(JOY_BUTTON_START in buttons, "Start leaves gameplay")
|
|
for face in [JOY_BUTTON_A, JOY_BUTTON_B, JOY_BUTTON_X, JOY_BUTTON_Y]:
|
|
assert_true(face not in buttons, "leave_gameplay must not use a face button")
|
|
|
|
|
|
func test_ball_cam_has_its_own_action_off_ui_accept() -> void:
|
|
# Ball-cam used to ride ui_accept, which project.godot now binds to A for
|
|
# menu confirmation. A dedicated action keeps A purely a menu button and
|
|
# lets the camera toggle be rebound like anything else.
|
|
assert_true(InputMap.has_action("toggle_ball_cam"), "toggle_ball_cam exists")
|
|
var joypad := _joypad_events("toggle_ball_cam")
|
|
assert_true(joypad.size() == 1, "toggle_ball_cam has one joypad binding")
|
|
if joypad.size() == 1:
|
|
assert_true(
|
|
joypad[0] is InputEventJoypadButton and joypad[0].button_index != JOY_BUTTON_A,
|
|
"toggle_ball_cam is not on A (menu confirm)"
|
|
)
|
|
|
|
|
|
func test_thrust_is_on_the_triggers() -> void:
|
|
# The requested layout, asserted where it is load-bearing: triggers are the
|
|
# only analog inputs on the thrust axis, so binding them to buttons instead
|
|
# would silently cost proportional throttle without failing anything.
|
|
var forward := _joypad_events("move_forward")
|
|
var back := _joypad_events("move_back")
|
|
assert_true(forward.size() == 1 and forward[0] is InputEventJoypadMotion, "move_forward is an axis")
|
|
assert_true(back.size() == 1 and back[0] is InputEventJoypadMotion, "move_back is an axis")
|
|
if forward.size() == 1 and forward[0] is InputEventJoypadMotion:
|
|
assert_eq(forward[0].axis, JOY_AXIS_TRIGGER_RIGHT, "move_forward axis")
|
|
if back.size() == 1 and back[0] is InputEventJoypadMotion:
|
|
assert_eq(back[0].axis, JOY_AXIS_TRIGGER_LEFT, "move_back axis")
|
|
|
|
|
|
func test_pitch_is_on_the_left_stick_nose_down_when_pushed_forward() -> void:
|
|
var down := _joypad_events("pitch_down")
|
|
var up := _joypad_events("pitch_up")
|
|
assert_true(down.size() == 1 and down[0] is InputEventJoypadMotion, "pitch_down is an axis")
|
|
assert_true(up.size() == 1 and up[0] is InputEventJoypadMotion, "pitch_up is an axis")
|
|
if down.size() == 1 and down[0] is InputEventJoypadMotion:
|
|
assert_eq(down[0].axis, JOY_AXIS_LEFT_Y, "pitch_down axis")
|
|
# Godot reports a stick pushed away from the player as negative Y.
|
|
assert_true(down[0].axis_value < 0.0, "stick forward pitches the nose down")
|
|
if up.size() == 1 and up[0] is InputEventJoypadMotion:
|
|
assert_eq(up[0].axis, JOY_AXIS_LEFT_Y, "pitch_up axis")
|
|
assert_true(up[0].axis_value > 0.0, "stick back pitches the nose up")
|
|
|
|
|
|
func test_all_rotation_lives_on_the_left_stick() -> void:
|
|
# Yaw and pitch belong on the same stick. Splitting them across two sticks
|
|
# (yaw left, pitch right) is playable in the sense that every input works,
|
|
# so nothing here failed when it was wrong — it just felt broken, because
|
|
# each stick had a dead axis. Asserting both are on the right stick is what
|
|
# pins the 6DOF convention down.
|
|
for action in ["turn_left", "turn_right"]:
|
|
var events := _joypad_events(action)
|
|
assert_true(events.size() == 1 and events[0] is InputEventJoypadMotion, "%s is an axis" % action)
|
|
if events.size() == 1 and events[0] is InputEventJoypadMotion:
|
|
assert_eq(events[0].axis, JOY_AXIS_LEFT_X, "%s axis" % action)
|
|
for action in ["pitch_up", "pitch_down"]:
|
|
var events := _joypad_events(action)
|
|
if events.size() == 1 and events[0] is InputEventJoypadMotion:
|
|
assert_eq(events[0].axis, JOY_AXIS_LEFT_Y, "%s axis" % action)
|
|
|
|
|
|
func test_translation_is_analog_on_the_right_stick_and_triggers() -> void:
|
|
# Six degrees of freedom onto the pad's six analog axes. Strafe and
|
|
# vertical were digital buttons at first, which cost proportional control
|
|
# without failing anything — a button binding here still "works", it just
|
|
# gives full power or nothing, so only checking the event type catches it.
|
|
var expected := {
|
|
"move_left": JOY_AXIS_RIGHT_X, "move_right": JOY_AXIS_RIGHT_X,
|
|
"move_up": JOY_AXIS_RIGHT_Y, "move_down": JOY_AXIS_RIGHT_Y,
|
|
"move_forward": JOY_AXIS_TRIGGER_RIGHT, "move_back": JOY_AXIS_TRIGGER_LEFT,
|
|
}
|
|
for action in expected:
|
|
var events := _joypad_events(action)
|
|
assert_true(
|
|
events.size() == 1 and events[0] is InputEventJoypadMotion,
|
|
"%s is analog, not a button" % action
|
|
)
|
|
if events.size() == 1 and events[0] is InputEventJoypadMotion:
|
|
assert_eq(events[0].axis, expected[action], "%s axis" % action)
|
|
|
|
|
|
func test_pushing_the_right_stick_up_thrusts_up() -> void:
|
|
# Godot reports a stick pushed away from the player as negative Y, so the
|
|
# intuitive direction needs the negative half — easy to get backwards, and
|
|
# inverted vertical thrust is not something any other assertion notices.
|
|
var up := _joypad_events("move_up")
|
|
var down := _joypad_events("move_down")
|
|
if up.size() == 1 and up[0] is InputEventJoypadMotion:
|
|
assert_true(up[0].axis_value < 0.0, "stick up thrusts up")
|
|
if down.size() == 1 and down[0] is InputEventJoypadMotion:
|
|
assert_true(down[0].axis_value > 0.0, "stick down thrusts down")
|
|
|
|
|
|
func test_roll_is_on_the_shoulder_buttons() -> void:
|
|
var expected := {"roll_left": JOY_BUTTON_LEFT_SHOULDER, "roll_right": JOY_BUTTON_RIGHT_SHOULDER}
|
|
for action in expected:
|
|
var events := _joypad_events(action)
|
|
assert_true(events.size() == 1 and events[0] is InputEventJoypadButton, "%s is a button" % action)
|
|
if events.size() == 1 and events[0] is InputEventJoypadButton:
|
|
assert_eq(events[0].button_index, expected[action], "%s button" % action)
|
|
|
|
|
|
func test_the_face_buttons_are_free_for_menus() -> void:
|
|
# A/B/X/Y carry no flight action, which is what lets ui_accept keep A and
|
|
# keeps a stray face-button press from doing something during a match.
|
|
for entry in InputSettings.ACTIONS:
|
|
var bound := InputSettings.get_binding(entry["action"], InputSettings.DEVICE_JOYPAD)
|
|
if bound is InputEventJoypadButton:
|
|
assert_true(
|
|
bound.button_index not in [JOY_BUTTON_A, JOY_BUTTON_B, JOY_BUTTON_X, JOY_BUTTON_Y],
|
|
"%s must not use a face button" % entry["action"]
|
|
)
|
|
|
|
|
|
func test_event_dict_round_trip_preserves_every_default() -> void:
|
|
for entry in InputSettings.ACTIONS:
|
|
for device in [InputSettings.DEVICE_KEYBOARD, InputSettings.DEVICE_JOYPAD]:
|
|
var original := InputSettings.get_default_binding(entry["action"], device)
|
|
assert_true(original != null, "%s/%s has a default" % [entry["action"], device])
|
|
if original == null:
|
|
continue
|
|
var restored := InputSettings.event_from_dict(InputSettings.event_to_dict(original))
|
|
assert_true(restored != null, "%s/%s round-trips to an event" % [entry["action"], device])
|
|
if restored != null:
|
|
assert_true(
|
|
InputSettings.events_match(original, restored),
|
|
"%s/%s round-trips to an equal event" % [entry["action"], device]
|
|
)
|
|
|
|
|
|
func test_event_from_dict_rejects_junk() -> void:
|
|
# A save file from a newer build, or a hand-edited one, must degrade to
|
|
# "unbound" rather than taking the game down before the player can reach
|
|
# the Controls tab to fix it.
|
|
assert_true(InputSettings.event_from_dict({}) == null, "empty dict is not an event")
|
|
assert_true(InputSettings.event_from_dict({"type": "mouse"}) == null, "unknown type is not an event")
|
|
assert_true(InputSettings.event_from_dict({"type": "key"}) == null, "keycode-less key is not an event")
|
|
assert_true(
|
|
InputSettings.event_from_dict({"type": "joy_axis", "axis": 3, "value": 0.0}) == null,
|
|
"a centred axis is not an event"
|
|
)
|
|
|
|
|
|
func test_axis_bindings_are_distinguished_by_direction() -> void:
|
|
# events_match must NOT collapse the two halves of one axis, or binding
|
|
# pitch-up would silently unbind pitch-down as a "conflict".
|
|
var up := InputEventJoypadMotion.new()
|
|
up.axis = JOY_AXIS_RIGHT_Y
|
|
up.axis_value = 1.0
|
|
var down := InputEventJoypadMotion.new()
|
|
down.axis = JOY_AXIS_RIGHT_Y
|
|
down.axis_value = -1.0
|
|
assert_true(not InputSettings.events_match(up, down), "opposite axis halves are different bindings")
|
|
assert_true(InputSettings.events_match(up, up), "an axis binding matches itself")
|
|
|
|
|
|
func test_set_binding_changes_the_live_input_map() -> void:
|
|
var rebound := InputEventKey.new()
|
|
rebound.physical_keycode = KEY_F # not used by any default binding
|
|
InputSettings.set_binding("move_forward", rebound)
|
|
|
|
var found := false
|
|
for event in InputMap.action_get_events("move_forward"):
|
|
if event is InputEventKey and event.physical_keycode == KEY_F:
|
|
found = true
|
|
assert_true(found, "the rebound key reaches InputMap")
|
|
assert_true(InputSettings.has_override("move_forward"), "the rebind is recorded as an override")
|
|
|
|
# The joypad half must survive a keyboard-only rebind.
|
|
assert_true(not _joypad_events("move_forward").is_empty(), "rebinding the key keeps the trigger")
|
|
|
|
InputSettings.reset_all()
|
|
|
|
|
|
func test_set_binding_displaces_the_conflicting_action() -> void:
|
|
# Binding X to an input already in use must report and clear the previous
|
|
# owner, not leave both bound and let the player wonder why two things fire.
|
|
var shared := InputSettings.get_binding("move_left", InputSettings.DEVICE_KEYBOARD)
|
|
assert_true(shared != null, "move_left has a keyboard binding to steal")
|
|
if shared == null:
|
|
return
|
|
|
|
var displaced := InputSettings.set_binding("move_right", shared)
|
|
assert_true(displaced.has("move_left"), "the displaced action is reported")
|
|
assert_true(
|
|
InputSettings.get_binding("move_left", InputSettings.DEVICE_KEYBOARD) == null,
|
|
"the displaced action is actually unbound"
|
|
)
|
|
|
|
InputSettings.reset_all()
|
|
|
|
|
|
func test_reset_all_restores_the_project_defaults() -> void:
|
|
var before := InputSettings.binding_text("move_up", InputSettings.DEVICE_JOYPAD)
|
|
var rebound := InputEventJoypadButton.new()
|
|
rebound.button_index = JOY_BUTTON_BACK
|
|
InputSettings.set_binding("move_up", rebound)
|
|
assert_true(
|
|
InputSettings.binding_text("move_up", InputSettings.DEVICE_JOYPAD) != before,
|
|
"the rebind took effect"
|
|
)
|
|
|
|
InputSettings.reset_all()
|
|
assert_eq(
|
|
InputSettings.binding_text("move_up", InputSettings.DEVICE_JOYPAD),
|
|
before,
|
|
"reset_all restores the default binding"
|
|
)
|
|
assert_true(not InputSettings.has_override("move_up"), "reset_all clears the override")
|
|
|
|
|
|
func test_bindings_survive_a_save_and_reload() -> void:
|
|
# The end-to-end persistence path, which nothing else covers: a rebind that
|
|
# does not survive a restart is the single most visible way this feature can
|
|
# fail, and it fails silently — the game runs fine, just on the defaults.
|
|
#
|
|
# This writes the real user://input.cfg, so the player's own file is saved
|
|
# and put back. Restoring it is not optional: the test suite shares a
|
|
# user:// directory with the game.
|
|
var had_file := FileAccess.file_exists(InputSettings.SETTINGS_PATH)
|
|
var original := ""
|
|
if had_file:
|
|
original = FileAccess.get_file_as_string(InputSettings.SETTINGS_PATH)
|
|
|
|
var rebound := InputEventJoypadButton.new()
|
|
rebound.button_index = JOY_BUTTON_BACK
|
|
InputSettings.set_binding("turbo", rebound)
|
|
InputSettings.invert_pitch = true
|
|
InputSettings.save()
|
|
|
|
# Drop the in-memory state the way a fresh launch would, then reload.
|
|
InputSettings.reset_all()
|
|
assert_true(not InputSettings.has_override("turbo"), "state cleared before reload")
|
|
InputSettings._load()
|
|
InputSettings.apply()
|
|
|
|
assert_true(InputSettings.has_override("turbo"), "the override came back from disk")
|
|
var loaded := InputSettings.get_binding("turbo", InputSettings.DEVICE_JOYPAD)
|
|
assert_true(loaded != null, "the reloaded binding is an event")
|
|
if loaded != null:
|
|
assert_true(InputSettings.events_match(loaded, rebound), "the reloaded binding matches what was saved")
|
|
assert_true(InputSettings.invert_pitch, "invert_pitch survives a reload")
|
|
|
|
# A deliberately-cleared binding must stay cleared across a restart. This is
|
|
# the case that distinguishes a real "unbound" record from an absent one:
|
|
# ConfigFile.set_value() erases a key whose value is null, so a naive
|
|
# implementation silently restores the default here instead.
|
|
InputSettings.clear_binding("roll_left", InputSettings.DEVICE_JOYPAD)
|
|
InputSettings.save()
|
|
InputSettings.reset_all()
|
|
InputSettings._load()
|
|
InputSettings.apply()
|
|
assert_true(
|
|
InputSettings.get_binding("roll_left", InputSettings.DEVICE_JOYPAD) == null,
|
|
"an unbound action stays unbound across a reload"
|
|
)
|
|
assert_true(
|
|
_joypad_events("roll_left").is_empty(),
|
|
"the unbound action has no joypad event in InputMap after a reload"
|
|
)
|
|
|
|
# And it must actually be live in InputMap, not merely remembered.
|
|
var live := false
|
|
for event in InputMap.action_get_events("turbo"):
|
|
if event is InputEventJoypadButton and event.button_index == JOY_BUTTON_BACK:
|
|
live = true
|
|
assert_true(live, "the reloaded binding is applied to InputMap")
|
|
|
|
InputSettings.reset_all()
|
|
if had_file:
|
|
var restore := FileAccess.open(InputSettings.SETTINGS_PATH, FileAccess.WRITE)
|
|
if restore != null:
|
|
restore.store_string(original)
|
|
restore.close()
|
|
InputSettings._load()
|
|
InputSettings.apply()
|
|
else:
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(InputSettings.SETTINGS_PATH))
|
|
|
|
|
|
func test_invert_pitch_drives_pitch_sign() -> void:
|
|
var restore := InputSettings.invert_pitch
|
|
InputSettings.invert_pitch = false
|
|
assert_eq(InputSettings.pitch_sign(), 1.0, "default pitch sign")
|
|
InputSettings.invert_pitch = true
|
|
assert_eq(InputSettings.pitch_sign(), -1.0, "inverted pitch sign")
|
|
InputSettings.invert_pitch = restore
|
|
|
|
|
|
func test_every_default_binding_has_readable_text() -> void:
|
|
# A rebind row showing "" or "Joypad Button 9 (Left Shoulder)" is a UI bug
|
|
# that no other assertion here would catch.
|
|
for entry in InputSettings.ACTIONS:
|
|
for device in [InputSettings.DEVICE_KEYBOARD, InputSettings.DEVICE_JOYPAD]:
|
|
var text := InputSettings.binding_text(entry["action"], device)
|
|
assert_true(
|
|
text != "" and text != "Unbound",
|
|
"%s/%s has a readable label (got %s)" % [entry["action"], device, text]
|
|
)
|
|
|
|
|
|
func test_device_kind_of_rejects_events_it_cannot_bind() -> void:
|
|
assert_eq(InputSettings.device_kind_of(InputEventMouseButton.new()), "", "mouse is not a bindable device")
|
|
assert_eq(InputSettings.device_kind_of(InputEventKey.new()), InputSettings.DEVICE_KEYBOARD, "key device")
|
|
assert_eq(
|
|
InputSettings.device_kind_of(InputEventJoypadMotion.new()),
|
|
InputSettings.DEVICE_JOYPAD,
|
|
"joypad motion device"
|
|
)
|