extends SceneTree # Headless gameplay test: run with # godot --headless -s res://tests/test_pivot_controls.gd # Covers the fleet-pivot buttons: # 1. HOLDING one turns the formation continuously, not once per press # 2. a brief tap still produces a clear minimum step # 3. immediately repeating a tap grants nothing extra until the cooldown # clears (#72) - spamming taps must never rotate faster than holding # the button would # 4. pivoting works while the fleet is under way, i.e. the new layout # survives the joystick's per-frame steering updates instead of being # overwritten by the old offsets var scene: Node var fleet: Node var controller: Node var cw: IconButton var stage: int = 0 var elapsed: float = 0.0 var angle_at_hold_start: float = 0.0 var hold_amount_at_start: float = 0.0 var angle_after_hold: float = 0.0 # GameState (an autoload) is not available as a bare identifier from a # script that extends SceneTree - fetch it via the tree root instead. var game_state: Node func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_formation() scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") controller = scene cw = scene.get_node("UI/PivotCWButton") func _angle() -> float: return game_state.formation[1].angle() func _process(delta: float) -> bool: elapsed += delta match stage: 0: scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) # Crew the fleet HERE, not in _initialize(): work done there can # be undone by the engine's deferred tree-entry finalization, the # same reason the set_process(false) calls above sit at this # point. A new game flies one vessel now, and the checks below # index ships[1]. # #219: berths come from Fleet Offices now, not from research. Three of # them buy the same four-vessel fleet the doctrines used to. Left # unstocked deliberately - upkeep is not charged for the first # 25 seconds, and nothing here runs that long. for _berth in range(3): game_state.add_building(GameState.BUILDING_FLEET_OFFICE, Vector2(3000.0 + 400.0 * float(_berth), 3000.0)) fleet.spawn_ships(5, Vector2.ZERO) # Hold the button down without releasing it. angle_at_hold_start = _angle() cw.pressed_down = true elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.6: return false angle_after_hold = _angle() var turned: float = angle_difference(angle_at_hold_start, angle_after_hold) # 0.6s at PIVOT_SPEED should be far beyond a single tap step. var held_ok: bool = absf(turned) > controller.PIVOT_MIN_STEP * 1.5 print("held 0.6s -> turned %.3f rad (min step %.3f) held_ok=%s" % [ absf(turned), controller.PIVOT_MIN_STEP, held_ok ]) if not held_ok: print("TEST_PIVOT_CONTROLS: FAIL (holding did not turn continuously)") quit(1) return true cw.pressed_down = false controller.pivot_hold_amount = 0.0 # --- brief tap ------------------------------------------------ var before_tap: float = _angle() controller._finish_pivot_tap(1.0) var tapped: float = absf(angle_difference(before_tap, _angle())) var tap_ok: bool = tapped >= controller.PIVOT_MIN_STEP - 0.001 print("tap -> turned %.3f rad tap_ok=%s" % [tapped, tap_ok]) if not tap_ok: print("TEST_PIVOT_CONTROLS: FAIL (tap did not produce the minimum step)") quit(1) return true # --- #72: spamming taps grants nothing extra during the cooldown --- var before_spam: float = _angle() for i in range(5): controller._finish_pivot_tap(1.0) var spammed: float = absf(angle_difference(before_spam, _angle())) print("5 taps back-to-back -> turned %.4f rad (expected ~0, cooldown not yet cleared)" % spammed) if spammed > 0.001: print("TEST_PIVOT_CONTROLS: FAIL (tap-spam granted rotation during the cooldown)") quit(1) return true # --- pivot WHILE the joystick is held ------------------------- # Two fingers at once. This is routed through the same raw-touch # path the real device uses, because Godot's Control GUI treats # touch as one emulated pointer and would not deliver a second # simultaneous finger to a button - the exact reason pivoting # while steering used to do nothing. var joy: TouchJoystick = scene.get_node("UI/Joystick") var stick_down: InputEventScreenTouch = InputEventScreenTouch.new() stick_down.index = 0 stick_down.pressed = true stick_down.position = joy.global_position + joy.size / 2.0 controller._handle_touch(stick_down) var stick_drag: InputEventScreenDrag = InputEventScreenDrag.new() stick_drag.index = 0 stick_drag.position = joy.global_position + joy.size / 2.0 + Vector2(joy.max_radius, 0.0) controller._handle_drag(stick_drag) if joy.direction.x < 0.9: print("TEST_PIVOT_CONTROLS: FAIL (stick did not engage)") quit(1) return true # Second finger lands on the clockwise pivot button. var pivot_down: InputEventScreenTouch = InputEventScreenTouch.new() pivot_down.index = 1 pivot_down.pressed = true pivot_down.position = cw.global_position + cw.size / 2.0 controller._handle_touch(pivot_down) var both_ok: bool = cw.pressed_down and joy.direction.x > 0.9 print("simultaneous: stick=%s pivot_held=%s" % [joy.direction, cw.pressed_down]) if not both_ok: print("TEST_PIVOT_CONTROLS: FAIL (cannot pivot while steering)") quit(1) return true angle_at_hold_start = _angle() hold_amount_at_start = controller.pivot_hold_amount elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.4: return false # Both fingers still down: the formation must have kept turning # while the stick kept re-issuing move orders every frame. # The ACCUMULATED turn, not the angle difference. Comparing # formation angles wraps at +/-pi, so a hold that happened to # rotate a full turn read as no rotation at all - and how far it # gets in 0.4s depends on frame times, so this failed only under # load. pivot_hold_amount is the same quantity without the wrap. var turned_while_steering: float = controller.pivot_hold_amount - hold_amount_at_start print("turned %.3f rad while steering" % turned_while_steering) if turned_while_steering < 0.05: print("TEST_PIVOT_CONTROLS: FAIL (no rotation while the stick was held)") quit(1) return true # Steering is re-issued every frame; pivot in the middle of it. fleet.move_in_direction(Vector2.RIGHT) var before_moving_pivot: Vector2 = game_state.formation[1] controller._pivot(PI / 2.0) var after: Vector2 = game_state.formation[1] fleet.move_in_direction(Vector2.RIGHT) # The ship's target must reflect the NEW offset even though # steering re-issued move orders straight afterwards. var expected: Vector2 = fleet.fleet_target + after var followed: bool = fleet.ships[1].move_target.distance_to(expected) < 0.01 var rotated: bool = after.distance_to(before_moving_pivot.rotated(PI / 2.0)) < 0.01 print("moving pivot: %s -> %s rotated=%s ship_target_ok=%s" % [ before_moving_pivot, after, rotated, followed ]) if rotated and followed: print("TEST_PIVOT_CONTROLS: PASS") quit(0) else: print("TEST_PIVOT_CONTROLS: FAIL (pivot lost while moving)") quit(1) return true return false