diff --git a/controllers/GameController.gd b/controllers/GameController.gd index 6e2aa03..168774c 100644 --- a/controllers/GameController.gd +++ b/controllers/GameController.gd @@ -383,14 +383,14 @@ func _unhandled_input(event: InputEvent) -> void: _handle_key(event) elif event is InputEventMouseMotion: _note_keyboard() - if speed_fader != null: - speed_fader.handle_mouse_motion(event) elif event is InputEventMouseButton: _note_keyboard() - # #118: the fader gets first refusal, so dragging the boost does not - # also pan or zoom the world behind it. - if speed_fader != null and speed_fader.handle_mouse_button(event): - return + # The speed fader used to be driven from here, and could not work: + # MouseCombatController is dispatched before this node (it is a + # child, and this is the root) and marks every left press handled to + # fire the guns, so the press never arrived. It listens for its own + # mouse events now - see SpeedFader._unhandled_input - and claims + # them from the UI layer, ahead of the combat controllers. if not event.pressed: return # Wheel zoom, the desktop equivalent of the pinch. diff --git a/tests/test_desktop_controls.gd b/tests/test_desktop_controls.gd index 4fad953..895ff43 100644 --- a/tests/test_desktop_controls.gd +++ b/tests/test_desktop_controls.gd @@ -10,16 +10,19 @@ extends SceneTree # The boost was unreachable on a desktop and the formation could not be # rearranged at all. # -# Driven through the REAL routing in each case (GameController 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. +# 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 fader claims the press, so dragging the boost does not also zoom -# the world behind it +# 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 @@ -67,6 +70,31 @@ func _fail(message: String) -> bool: 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 @@ -112,22 +140,29 @@ func _process(delta: float) -> bool: 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) - _click(bottom, true, scene) + _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 - _move(top, scene) + _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) - _click(top, false, scene) + _push_click(top, false) if fader.is_active(): return _fail("the fader is still held after the button came up") - _move(bottom, scene) + _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" \ @@ -135,10 +170,13 @@ func _process(delta: float) -> bool: # --- B. it claims the press ------------------------------ var zoom_before: float = scene.get_node("Camera2D").zoom.x - _click(bottom, true, scene) - _click(bottom, false, scene) + _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 ------------------------- diff --git a/ui/SpeedFader.gd b/ui/SpeedFader.gd index bebda79..7affc29 100644 --- a/ui/SpeedFader.gd +++ b/ui/SpeedFader.gd @@ -101,6 +101,28 @@ func handle_mouse_motion(event: InputEventMouseMotion) -> bool: return true +# ...and listening for those events HERE rather than being handed them by +# GameController, which is what #118 did. +# +# Unhandled input reaches the last nodes in the tree first, and Main - where +# that dispatch lives - is the root, so it is served last of all. +# MouseCombatController is a child of it and claims every left press to fire +# the fleet's guns, which meant a click on the fader shot at the fader +# instead of taking hold of it: the knob never moved on a desktop. The UI +# layer sits after the combat controllers, so asking for the event here puts +# the control the player is actually pointing at ahead of the guns behind it. +# +# Touch still comes through GameController: nothing competes for a finger, +# and the joystick/fire stick ordering it arbitrates is real. +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + if handle_mouse_button(event): + get_viewport().set_input_as_handled() + elif event is InputEventMouseMotion: + if handle_mouse_motion(event): + get_viewport().set_input_as_handled() + + func handle_touch(event: InputEventScreenTouch) -> bool: return _press(event.pressed, event.position, event.index)