extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_joystick_control.gd # Verifies the virtual joystick steers the fleet: holding the stick in a # direction flies the fleet that way, and releasing brings it to a stop # rather than freezing it mid-flight. Replaces the old tap-to-move test, # since tapping no longer issues move orders. # # Touch events are handed to the joystick directly rather than through # Input.parse_input_event: headless mode's placeholder window rescales # synthetic touch coordinates by an environment-specific factor, which # would land them outside the joystick's rect for reasons that have nothing # to do with the behaviour under test. var main_scene: Node var fleet: Node var joystick: TouchJoystick var stage: int = 0 var elapsed: float = 0.0 var start_centroid: Vector2 var moving_centroid: Vector2 var stop_check_frames: int = 0 # Release-coast tracking (Forgejo #13). var released: bool = false var speed_at_release: float = 0.0 var previous_speed: float = 0.0 var accelerated_after_release: bool = false func _initialize() -> void: var packed: PackedScene = load("res://Main.tscn") main_scene = packed.instantiate() root.add_child(main_scene) fleet = main_scene.get_node("Fleet") joystick = main_scene.get_node("UI/Joystick") func _press(local: Vector2) -> void: var ev: InputEventScreenTouch = InputEventScreenTouch.new() ev.index = 0 ev.pressed = true ev.position = joystick.global_position + local joystick.handle_touch(ev) func _drag(local: Vector2) -> void: var ev: InputEventScreenDrag = InputEventScreenDrag.new() ev.index = 0 ev.position = joystick.global_position + local joystick.handle_drag(ev) func _release(local: Vector2) -> void: var ev: InputEventScreenTouch = InputEventScreenTouch.new() ev.index = 0 ev.pressed = false ev.position = joystick.global_position + local joystick.handle_touch(ev) func _max_ship_speed() -> float: var top: float = 0.0 for ship in fleet.ships: top = maxf(top, ship.velocity.length()) return top func _process(delta: float) -> bool: elapsed += delta # Releasing the stick must be ONE continuous decelerating coast. It used # to stop dead and then accelerate into its own stopping distance, # halting a second time - felt in game as a pause followed by a small # extra lurch (#13). The ships were being aimed at a braking point ahead # while their own velocity was ~0, because the anchor had been carrying # them, so they started from rest. if released: var now: float = _max_ship_speed() if now > previous_speed + 1.0: accelerated_after_release = true previous_speed = now match stage: 0: main_scene.get_node("AsteroidField").set_process(false) main_scene.get_node("EnemySpawner").set_process(false) start_centroid = fleet.get_centroid() # The joystick receives touches via GameController's # _unhandled_input, which never fires for a Control that # consumes them through the GUI system first. With Godot's # default mouse_filter (STOP) the stick silently does nothing # on a real device - it must be IGNORE. Checked structurally # because the rest of this test drives handle_touch() directly # and so cannot catch it. if joystick.mouse_filter != Control.MOUSE_FILTER_IGNORE: print("joystick mouse_filter=%d (needs IGNORE=%d)" % [ joystick.mouse_filter, Control.MOUSE_FILTER_IGNORE ]) print("TEST_JOYSTICK_CONTROL: FAIL (stick would swallow its own touches)") quit(1) return true # Push the stick hard to the right. var centre: Vector2 = joystick.size / 2.0 _press(centre) _drag(centre + Vector2(joystick.max_radius, 0.0)) if joystick.direction.x < 0.9: print("joystick direction wrong: %s" % joystick.direction) print("TEST_JOYSTICK_CONTROL: FAIL (stick did not register)") quit(1) return true elapsed = 0.0 stage = 1 return false 1: if elapsed < 1.5: return false moving_centroid = fleet.get_centroid() var moved_right: bool = moving_centroid.x - start_centroid.x > 100.0 var stayed_level: bool = abs(moving_centroid.y - start_centroid.y) < 80.0 print("moved from %s to %s (right=%s level=%s)" % [ start_centroid, moving_centroid, moved_right, stayed_level ]) if not (moved_right and stayed_level): print("TEST_JOYSTICK_CONTROL: FAIL (fleet did not follow the stick)") quit(1) return true _release(joystick.size / 2.0) released = true speed_at_release = _max_ship_speed() previous_speed = speed_at_release if joystick.direction != Vector2.ZERO: print("TEST_JOYSTICK_CONTROL: FAIL (direction not cleared on release)") quit(1) return true elapsed = 0.0 stage = 2 return false 2: # After release the fleet should coast to a halt, not freeze # instantly and not keep flying. if elapsed < 4.0: return false var speed: float = 0.0 for ship in fleet.ships: speed = max(speed, ship.velocity.length()) # Carrying the fleet's cruise speed, not starting from a standstill. if speed_at_release < fleet.cruise_speed * 0.5: print("speed one frame after release: %.1f (cruising at %.1f)" % [ speed_at_release, fleet.cruise_speed ]) print("TEST_JOYSTICK_CONTROL: FAIL (the fleet stopped dead on release instead of coasting)") quit(1) return true if accelerated_after_release: print("TEST_JOYSTICK_CONTROL: FAIL (the fleet sped up again after release - pause then lurch, #13)") quit(1) return true print("release coast: %.1f -> 0 with no re-acceleration" % speed_at_release) var stopped: bool = speed < 1.0 # Not merely "moved forward at all": the fleet cruises at # cruise_speed and brakes at Ship.acceleration, so the coast is # cruise_speed^2 / (2 * acceleration) ~ 75 units. Asserting a # real distance is what makes this a test of coasting - the old # `> moving_centroid.x` was satisfied by any float wobble, and # passed happily while the fleet stopped dead. var coast: float = fleet.get_centroid().x - moving_centroid.x var coasted: bool = coast > 40.0 print("after release: max_speed=%.3f stopped=%s coasted=%.1f units (expected ~%.0f)" % [ speed, stopped, coast, (fleet.cruise_speed * fleet.cruise_speed) / (2.0 * fleet.ships[0].acceleration) ]) if stopped and coasted: print("TEST_JOYSTICK_CONTROL: PASS") quit(0) else: print("TEST_JOYSTICK_CONTROL: FAIL (did not coast to a stop)") quit(1) return true return false