extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_desktop_controls.gd # Forgejo #118 and #121: two raw-touch controls that a mouse could not # reach. # # Both read InputEventScreenTouch/ScreenDrag, and the project no longer turns # clicks into them - the same routing gap #93 found in the research tree. # The boost was unreachable on a desktop and the formation could not be # rearranged at all. # # Driven through the REAL routing in each case - the viewport itself for the # fader, BuildController for the editor - rather than by calling the # controls directly, because the routing is what was missing: a test that # called handle_mouse_button() itself would have passed against both # reports. It did, in fact, and hid a second one: the fader was dispatched # from Main, which is the LAST node to see an unhandled press, so # MouseCombatController fired the guns with it and the knob never moved. # # Covers: # A. #118 - a click on the fader takes hold of it, dragging moves the # boost, and releasing lets go # B. the press stops at the fader: not the camera behind it, and not the # guns, which claim every other left press on a desktop # C. #121 - a click grabs a formation slot, moving drags it, and the # formation really changes # D. releasing drops the slot, and moving afterwards does not keep dragging # E. the wheel zooms the formation panel, which is what replaces the pinch var game_state: Node var scene: Node var controller: Node var fleet: Node var fader: Control var editor: Control var formation_menu: Control var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: # GameState is an autoload: a bare GameState. does not # compile in a SceneTree script, so the node is fetched by name instead. game_state = root.get_node("GameState") game_state.buildings.clear() game_state.extractors.clear() game_state.reset_progression() game_state.reset_waves() game_state.reset_formation() game_state.intro_active = false game_state.vessel_levels["unlock_distorder"] = 1 # #219: fleet capacity is built now - one berth free, one per Fleet # Office. This test flies more than one vessel, so it needs the offices # standing before the fleet spawns into them. load("res://tests/_berths.gd").grant(root.get_node("GameState"), 6) scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) current_scene = scene fleet = scene.get_node("Fleet") controller = scene.get_node("BuildController") fader = scene.get_node("UI/SpeedFader") editor = scene.get_node("UI/FormationMenu/Editor") formation_menu = scene.get_node("UI/FormationMenu") func _fail(message: String) -> bool: print("TEST_DESKTOP_CONTROLS: FAIL (%s)" % message) quit(1) return true # Pushed through the viewport, not handed to a node's own _unhandled_input: # WHICH node sees a press first is the thing under test. #118 shipped a fader # that only worked when a test called it directly - MouseCombatController # claims every left press to fire the guns, and it is dispatched well before # Main's own handler, so on a real desktop the click never arrived. # `true` keeps the coordinates in viewport space, which is the space the # controls are laid out in. func _push(event: InputEvent) -> void: root.push_input(event, true) func _push_click(at: Vector2, pressed: bool) -> void: var event: InputEventMouseButton = InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_LEFT event.pressed = pressed event.position = at _push(event) func _push_move(to: Vector2) -> void: var event: InputEventMouseMotion = InputEventMouseMotion.new() event.position = to _push(event) func _click(at: Vector2, pressed: bool, target: Node) -> void: var event: InputEventMouseButton = InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_LEFT event.pressed = pressed event.position = at target._unhandled_input(event) func _move(to: Vector2, target: Node) -> void: var event: InputEventMouseMotion = InputEventMouseMotion.new() event.position = to target._unhandled_input(event) func _wheel(button: int, at: Vector2) -> void: var event: InputEventMouseButton = InputEventMouseButton.new() event.button_index = button event.pressed = true event.position = at controller._unhandled_input(event) func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.3: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) # The fader only exists where the fleet can actually boost. fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([ GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_DISTORDER])) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.3: return false if not fader.visible: return _fail("the fleet has an Energy Propulser and the fader is still hidden") if game_state.max_fleet_speed_multiplier <= 1.0: return _fail("the boost has no range to move through") # --- A. click, drag, release ----------------------------- # With mouse aiming live, which is what a desktop always has: # the guns are the other thing a left press means, and they used # to take every one of them. var mouse_combat: Node = scene.get_node("MouseCombatController") mouse_combat.pointer_mode = mouse_combat.PointerMode.ON var top: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5, fader.track_margin + 2.0) var bottom: Vector2 = fader.global_position + Vector2(fader.size.x * 0.5, fader.size.y - fader.track_margin - 2.0) _push_click(bottom, true) if not fader.is_active(): return _fail("a click on the fader did not take hold of it") if mouse_combat.firing: return _fail("the click that grabbed the fader also fired the fleet's guns") var at_bottom: float = game_state.fleet_speed_multiplier _push_move(top) var at_top: float = game_state.fleet_speed_multiplier if at_top <= at_bottom: return _fail("dragging the fader from bottom to top left the boost at %.2f" % at_top) _push_click(top, false) if fader.is_active(): return _fail("the fader is still held after the button came up") _push_move(bottom) if not is_equal_approx(game_state.fleet_speed_multiplier, at_top): return _fail("the fader kept following the pointer after release") print("fader: %.2fx at the bottom, %.2fx at the top, released cleanly" \ % [at_bottom, at_top]) # --- B. it claims the press ------------------------------ var zoom_before: float = scene.get_node("Camera2D").zoom.x _push_click(bottom, true) _push_click(bottom, false) if not is_equal_approx(scene.get_node("Camera2D").zoom.x, zoom_before): return _fail("pressing the fader also moved the camera behind it") if mouse_combat.firing: return _fail("the fader's press reached the guns behind it") mouse_combat.pointer_mode = mouse_combat.PointerMode.AUTO print("the press stops at the fader; the world behind it is untouched") # --- C/D/E. the formation editor ------------------------- formation_menu.visible = true editor.queue_redraw() elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.2: return false var slot_before: Vector2 = game_state.formation[0] var grab: Vector2 = editor.global_position + editor._slot_screen_pos(0) _click(grab, true, controller) if editor.dragging_index != 0: return _fail("a click on the first formation slot grabbed %d" % editor.dragging_index) _move(grab + Vector2(70.0, 40.0), controller) if game_state.formation[0] == slot_before: return _fail("dragging the slot did not move it") var dragged: Vector2 = game_state.formation[0] print("slot 0 moved from %s to %s under a mouse" % [str(slot_before), str(dragged)]) # --- D. release lets go ---------------------------------- _click(grab + Vector2(70.0, 40.0), false, controller) if editor.dragging_index != -1: return _fail("the slot is still held after the button came up") _move(grab + Vector2(200.0, 200.0), controller) if game_state.formation[0] != dragged: return _fail("the slot kept following the pointer after release") print("released, and the slot stopped following") # --- E. the wheel zooms ---------------------------------- var scale_before: float = editor.scale_factor _wheel(MOUSE_BUTTON_WHEEL_UP, editor.global_position + editor.size * 0.5) if editor.scale_factor <= scale_before: return _fail("the wheel did not zoom the formation panel in") _wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5) _wheel(MOUSE_BUTTON_WHEEL_DOWN, editor.global_position + editor.size * 0.5) if editor.scale_factor >= scale_before: return _fail("the wheel did not zoom back out") print("the wheel zooms the panel both ways (%.2f -> %.2f)" \ % [scale_before, editor.scale_factor]) print("TEST_DESKTOP_CONTROLS: PASS") quit(0) return true return false