Clicking the fader did nothing on a desktop: it fired the guns instead. Unhandled input reaches the last nodes in the tree first, and the fader's mouse handling lived in GameController - the script on Main, the ROOT, so it was served last of all. MouseCombatController is a child of it and marks every left press handled to fire, so the press never arrived. The fader asks for its own mouse events now, from the UI layer, which sits after the combat controllers: the control the pointer is actually on gets the press ahead of the guns behind it. The handlers themselves are the same wrappers #118 wrote. The test for #118 could not see any of this - it handed the event straight to Main._unhandled_input, which is precisely the delivery the real game never makes, so it passed against the bug it was written for. It pushes through the viewport now, with mouse aiming on, and checks the guns stay quiet. Confirmed it fails without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QR2Mdk42ghEpeyXPe4UFyn
226 lines
8.8 KiB
GDScript
226 lines
8.8 KiB
GDScript
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.<instance member> 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
|