extends "res://tests/test_case.gd" # Covers PlayerShipController's translation of input actions into a ShipAction. # # The point of most of these is the *analog* path. The controller used to read # is_action_pressed(), which is a bool, so a half-pulled trigger and a fully # pulled one produced identical full thrust. A test that only ever pressed # actions at full strength could not tell the two implementations apart — so # these press at fractional strength, which only the get_action_strength() # version can reproduce. # # Input.action_press writes to the global input state, so every test must # release what it pressed before returning or it leaks into later cases. const ACTIONS_USED := [ "move_forward", "move_back", "move_left", "move_right", "move_up", "move_down", "turn_left", "turn_right", "pitch_up", "pitch_down", "roll_left", "roll_right", "turbo", ] func _controller() -> PlayerShipController: return PlayerShipController.new() func _release_all() -> void: for action in ACTIONS_USED: Input.action_release(action) func test_full_strength_matches_the_historical_digital_values() -> void: # The keyboard path must be unchanged by the move to analog: a held key # reports strength 1.0, so every axis lands on exactly ±1. var controller := _controller() Input.action_press("move_forward", 1.0) Input.action_press("move_right", 1.0) Input.action_press("move_up", 1.0) var action := controller.get_action() assert_almost_eq(action.thrust.z, 1.0, 0.001, "forward thrust") assert_almost_eq(action.thrust.x, 1.0, 0.001, "right thrust") assert_almost_eq(action.thrust.y, 1.0, 0.001, "up thrust") _release_all() Input.action_press("move_back", 1.0) Input.action_press("move_left", 1.0) Input.action_press("move_down", 1.0) action = controller.get_action() assert_almost_eq(action.thrust.z, -1.0, 0.001, "backward thrust") assert_almost_eq(action.thrust.x, -1.0, 0.001, "left thrust") assert_almost_eq(action.thrust.y, -1.0, 0.001, "down thrust") _release_all() func test_rotation_sign_conventions_are_unchanged() -> void: # Each action must move the ship the way its NAME says. The physics # directions were measured by driving a real Ship through ship.tscn rather # than reasoned about, because the right-hand rule is exactly the kind of # thing that reads as obvious and comes out backwards: # # rotation.x > 0 -> nose UP (torque about local +X) # rotation.y > 0 -> nose LEFT (torque about local +Y) # rotation.z > 0 -> banks LEFT (torque about local +Z) # # pitch was inverted against this for a long time — get_axis's arguments # were the wrong way round, so "pitch_down" raised the nose and the I/K keys # each did the opposite of their label. Nothing caught it because the sign # was self-consistent everywhere it was used; only comparing against the # physics reveals it. var controller := _controller() var restore := InputSettings.invert_pitch InputSettings.invert_pitch = false Input.action_press("turn_left", 1.0) Input.action_press("pitch_up", 1.0) Input.action_press("roll_left", 1.0) var action := controller.get_action() assert_almost_eq(action.rotation.y, 1.0, 0.001, "yaw left is positive") assert_almost_eq(action.rotation.x, 1.0, 0.001, "pitch UP is positive (nose up)") assert_almost_eq(action.rotation.z, 1.0, 0.001, "roll left is positive") _release_all() Input.action_press("turn_right", 1.0) Input.action_press("pitch_down", 1.0) Input.action_press("roll_right", 1.0) action = controller.get_action() assert_almost_eq(action.rotation.y, -1.0, 0.001, "yaw right is negative") assert_almost_eq(action.rotation.x, -1.0, 0.001, "pitch DOWN is negative (nose down)") assert_almost_eq(action.rotation.z, -1.0, 0.001, "roll right is negative") _release_all() InputSettings.invert_pitch = restore func test_partial_strength_produces_partial_thrust() -> void: # The analog assertion. A digital is_action_pressed() implementation would # return 1.0 here and fail. var controller := _controller() Input.action_press("move_forward", 0.5) assert_almost_eq(controller.get_action().thrust.z, 0.5, 0.001, "half trigger is half thrust") _release_all() Input.action_press("move_up", 0.25) assert_almost_eq(controller.get_action().thrust.y, 0.25, 0.001, "quarter deflection is quarter thrust") _release_all() Input.action_press("turn_left", 0.3) assert_almost_eq(controller.get_action().rotation.y, 0.3, 0.001, "partial stick is partial yaw") _release_all() func test_opposing_inputs_subtract_rather_than_saturate() -> void: # Both halves of one stick axis can report a strength at once; the result # must be their difference, not whichever was read last. var controller := _controller() Input.action_press("move_forward", 0.75) Input.action_press("move_back", 0.25) assert_almost_eq(controller.get_action().thrust.z, 0.5, 0.001, "opposed thrust subtracts") _release_all() Input.action_press("move_forward", 0.4) Input.action_press("move_back", 0.4) assert_almost_eq(controller.get_action().thrust.z, 0.0, 0.001, "equal opposed thrust cancels") _release_all() func test_no_input_is_a_zero_action() -> void: var controller := _controller() _release_all() var action := controller.get_action() assert_eq(action.thrust, Vector3.ZERO, "idle thrust") assert_eq(action.rotation, Vector3.ZERO, "idle rotation") assert_true(not action.turbo, "idle turbo") func test_invert_pitch_flips_only_the_pitch_axis() -> void: var controller := _controller() var restore := InputSettings.invert_pitch Input.action_press("pitch_down", 1.0) Input.action_press("turn_left", 1.0) InputSettings.invert_pitch = false var normal := controller.get_action().copy() InputSettings.invert_pitch = true var inverted := controller.get_action().copy() assert_almost_eq(inverted.rotation.x, -normal.rotation.x, 0.001, "invert flips pitch") assert_almost_eq(inverted.rotation.y, normal.rotation.y, 0.001, "invert leaves yaw alone") InputSettings.invert_pitch = restore _release_all() func test_turbo_is_a_boolean() -> void: var controller := _controller() Input.action_press("turbo", 1.0) assert_true(controller.get_action().turbo, "turbo held") Input.action_release("turbo") assert_true(not controller.get_action().turbo, "turbo released") _release_all() func test_the_returned_action_is_reused_between_ticks() -> void: # get_action() documents that it returns a reused instance and overwrites # every axis. Callers that keep an action past its tick must copy() it — # local_input_timeline.gd and the prediction ring rely on that contract, so # assert both halves of it. var controller := _controller() Input.action_press("move_forward", 1.0) var first := controller.get_action() _release_all() var second := controller.get_action() assert_true(first == second, "the same ShipAction instance is returned each tick") assert_almost_eq(second.thrust.z, 0.0, 0.001, "releasing clears the axis rather than leaving it stale")