#93: drive the research menu with a mouse, and close the hole under it
Two separate faults, both reported on the same ticket. The menu was inert under a mouse. Input reaches the tree as raw events through BuildController._unhandled_input, because a Control that consumes them through the GUI system swallows the drag, and the tree only ever read touch events. That was invisible while the project set emulate_touch_from_mouse and every click arrived as a synthetic touch; with that setting gone, nothing selects and nothing scrolls, and there is no other way to research anything on a desktop. The press and the pan are now the behaviour, with touch and mouse as two ways in to them. Motion pans only while the button is held - a hovering pointer must not drag the tree out from under the reader - and the wheel scrolls, which is the first thing a desktop player reaches for. The panel also still had the buy button's hole in it. That button went away in #83 and nothing moved down into the space, leaving ~180px of dead panel below the readout while the tree above it stayed cramped. The readout moves down 154px and the canvas takes the height: 1190px to 1344px. The test drives clicks through BuildController rather than into the view, since the missing piece was the routing - calling the view directly would have passed against the bug as reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
03b1c85569
commit
4883197ef3
5 changed files with 242 additions and 11 deletions
|
|
@ -343,6 +343,14 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
skill_tree.handle_touch(event)
|
skill_tree.handle_touch(event)
|
||||||
elif event is InputEventScreenDrag:
|
elif event is InputEventScreenDrag:
|
||||||
skill_tree.handle_drag(event)
|
skill_tree.handle_drag(event)
|
||||||
|
elif event is InputEventMouseButton:
|
||||||
|
# #93: a click is not a touch. The project no longer emulates one
|
||||||
|
# from the other, so without this branch the whole menu is inert
|
||||||
|
# under a mouse - nothing selects, nothing scrolls, and there is
|
||||||
|
# no other way to research anything on a desktop.
|
||||||
|
skill_tree.handle_mouse_button(event)
|
||||||
|
elif event is InputEventMouseMotion:
|
||||||
|
skill_tree.handle_mouse_motion(event)
|
||||||
# Handled either way. A touch that lands on the panel but misses a
|
# Handled either way. A touch that lands on the panel but misses a
|
||||||
# node is still a touch ON THE PANEL - letting it through moved the
|
# node is still a touch ON THE PANEL - letting it through moved the
|
||||||
# fleet and pinch-zoomed the world behind the menu, because
|
# fleet and pinch-zoomed the world behind the menu, because
|
||||||
|
|
|
||||||
181
tests/test_skill_menu.gd
Normal file
181
tests/test_skill_menu.gd
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless test: run with
|
||||||
|
# godot --headless -s res://tests/test_skill_menu.gd
|
||||||
|
# Forgejo #93, both halves of it.
|
||||||
|
#
|
||||||
|
# A mouse could not drive the research menu at all. Input reaches the tree
|
||||||
|
# through BuildController._unhandled_input as raw events (a Control that
|
||||||
|
# eats them through the GUI system would swallow the drag - see
|
||||||
|
# SkillTreeView's header), and the tree only ever read touch events. That
|
||||||
|
# was invisible for as long as the project set emulate_touch_from_mouse,
|
||||||
|
# which handed it a synthetic touch per click; with that setting gone, the
|
||||||
|
# menu is inert under a mouse and there is no other way to research
|
||||||
|
# anything on a desktop.
|
||||||
|
#
|
||||||
|
# The panel also still had a hole in it. The buy button below the readout
|
||||||
|
# went away in #83, and nothing moved into the space, so ~180px of empty
|
||||||
|
# panel sat under the text while the tree canvas above it stayed cramped.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. a left click selects a node, through the real routing in
|
||||||
|
# BuildController rather than by calling the view directly
|
||||||
|
# B. a second click on the SAME node researches it, which is #83's
|
||||||
|
# confirm gesture reached with a mouse
|
||||||
|
# C. holding the left button and moving scrolls the canvas, and moving
|
||||||
|
# with no button held does NOT - a hovering pointer must not drag the
|
||||||
|
# tree out from under the reader
|
||||||
|
# D. the wheel scrolls, in both directions
|
||||||
|
# E. the readout sits at the bottom of the panel and the tree canvas fills
|
||||||
|
# everything above it, so neither the dead band nor the cramped tree
|
||||||
|
# comes back
|
||||||
|
|
||||||
|
const GAP_TOLERANCE: float = 48.0
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var scene: Node
|
||||||
|
var controller: Node
|
||||||
|
var skill_menu: Control
|
||||||
|
var tree_view: Control
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.reset_formation()
|
||||||
|
game_state.rocks = 500
|
||||||
|
|
||||||
|
scene = load("res://world/Main.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
controller = scene.get_node("BuildController")
|
||||||
|
skill_menu = scene.get_node("UI/SkillMenu")
|
||||||
|
tree_view = scene.get_node("UI/SkillMenu/Tree")
|
||||||
|
|
||||||
|
|
||||||
|
func _click(at: Vector2, pressed: bool) -> void:
|
||||||
|
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
event.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
event.pressed = pressed
|
||||||
|
event.position = at
|
||||||
|
# Through the controller, not straight into the view: the missing piece
|
||||||
|
# was the ROUTING, so a test that called tree_view.handle_mouse_button()
|
||||||
|
# itself would have passed against the reported bug.
|
||||||
|
controller._unhandled_input(event)
|
||||||
|
|
||||||
|
|
||||||
|
func _wheel(button: int) -> void:
|
||||||
|
var event: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
event.button_index = button
|
||||||
|
event.pressed = true
|
||||||
|
event.position = tree_view.global_position + tree_view.size * 0.5
|
||||||
|
controller._unhandled_input(event)
|
||||||
|
|
||||||
|
|
||||||
|
func _move(relative: Vector2) -> void:
|
||||||
|
var event: InputEventMouseMotion = InputEventMouseMotion.new()
|
||||||
|
event.relative = relative
|
||||||
|
event.position = tree_view.global_position + tree_view.size * 0.5
|
||||||
|
controller._unhandled_input(event)
|
||||||
|
|
||||||
|
|
||||||
|
func _node_at(id: String) -> Vector2:
|
||||||
|
return tree_view.global_position + tree_view.node_screen_pos(game_state.skill_node(id))
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
if stage == 0:
|
||||||
|
scene.get_node("AsteroidField").set_process(false)
|
||||||
|
scene.get_node("EnemySpawner").set_process(false)
|
||||||
|
scene.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0))
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
if stage != 1 or elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
stage = 2
|
||||||
|
|
||||||
|
var mothership: Node2D = null
|
||||||
|
for building in scene.get_node("BuildingField").spawned:
|
||||||
|
if is_instance_valid(building) and building.building_type == GameState.BUILDING_MOTHERSHIP:
|
||||||
|
mothership = building
|
||||||
|
controller.current_building = mothership
|
||||||
|
controller._on_action_pressed()
|
||||||
|
if not skill_menu.visible:
|
||||||
|
return _fail("the mothership did not open the research menu")
|
||||||
|
|
||||||
|
# --- A. a click selects --------------------------------------------
|
||||||
|
var shipyards: Vector2 = _node_at("shipyards")
|
||||||
|
_click(shipyards, true)
|
||||||
|
if tree_view.selected_id != "shipyards":
|
||||||
|
return _fail("a left click on a node selected \"%s\" instead of it" % tree_view.selected_id)
|
||||||
|
_click(shipyards, false)
|
||||||
|
print("a left click selects the node under it")
|
||||||
|
|
||||||
|
# --- B. a second click researches ----------------------------------
|
||||||
|
if game_state.is_unlocked("shipyards"):
|
||||||
|
return _fail("shipyards was already researched, so the confirm proves nothing")
|
||||||
|
_click(shipyards, true)
|
||||||
|
_click(shipyards, false)
|
||||||
|
if not game_state.is_unlocked("shipyards"):
|
||||||
|
return _fail("a second click on the selected node did not research it")
|
||||||
|
print("a second click on the same node researches it")
|
||||||
|
|
||||||
|
# --- C. held drag scrolls, a bare hover does not --------------------
|
||||||
|
var before: float = tree_view.scroll_y
|
||||||
|
_move(Vector2(0.0, 120.0))
|
||||||
|
if not is_equal_approx(tree_view.scroll_y, before):
|
||||||
|
return _fail("the tree scrolled under a hovering pointer with no button held")
|
||||||
|
_click(_node_at("shipyards"), true)
|
||||||
|
_move(Vector2(0.0, 120.0))
|
||||||
|
var dragged: float = tree_view.scroll_y
|
||||||
|
if dragged >= before:
|
||||||
|
return _fail("holding the button and moving did not scroll the tree (%f -> %f)" \
|
||||||
|
% [before, dragged])
|
||||||
|
_click(_node_at("shipyards"), false)
|
||||||
|
_move(Vector2(0.0, 120.0))
|
||||||
|
if not is_equal_approx(tree_view.scroll_y, dragged):
|
||||||
|
return _fail("the tree kept scrolling after the button was released")
|
||||||
|
print("drag scrolls only while the button is held: %.0f -> %.0f" % [before, dragged])
|
||||||
|
|
||||||
|
# --- D. the wheel scrolls ------------------------------------------
|
||||||
|
_wheel(MOUSE_BUTTON_WHEEL_UP)
|
||||||
|
var wheeled: float = tree_view.scroll_y
|
||||||
|
if wheeled >= dragged:
|
||||||
|
return _fail("the wheel did not scroll the tree upward")
|
||||||
|
_wheel(MOUSE_BUTTON_WHEEL_DOWN)
|
||||||
|
if tree_view.scroll_y <= wheeled:
|
||||||
|
return _fail("the wheel did not scroll back down")
|
||||||
|
print("the wheel scrolls both ways")
|
||||||
|
|
||||||
|
# --- E. the panel has no dead band at the bottom --------------------
|
||||||
|
var detail: Control = scene.get_node("UI/SkillMenu/DetailLabel")
|
||||||
|
var status: Control = scene.get_node("UI/SkillMenu/StatusLabel")
|
||||||
|
var tree_bottom: float = tree_view.position.y + tree_view.size.y
|
||||||
|
var readout_gap: float = detail.position.y - tree_bottom
|
||||||
|
var floor_gap: float = skill_menu.size.y - (status.position.y + status.size.y)
|
||||||
|
print("tree %.0fpx tall, %.0fpx to the readout, %.0fpx of panel below it" \
|
||||||
|
% [tree_view.size.y, readout_gap, floor_gap])
|
||||||
|
if tree_view.size.y <= 0.0:
|
||||||
|
return _fail("the tree canvas has no height")
|
||||||
|
if readout_gap < 0.0:
|
||||||
|
return _fail("the tree canvas overlaps the readout below it")
|
||||||
|
if readout_gap > GAP_TOLERANCE:
|
||||||
|
return _fail("%.0fpx of unused panel between the tree and the readout" % readout_gap)
|
||||||
|
if floor_gap > GAP_TOLERANCE:
|
||||||
|
return _fail("%.0fpx of dead panel below the readout - the removed buy button's hole" \
|
||||||
|
% floor_gap)
|
||||||
|
|
||||||
|
print("TEST_SKILL_MENU: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_SKILL_MENU: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
1
tests/test_skill_menu.gd.uid
Normal file
1
tests/test_skill_menu.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://daagdu7tybuva
|
||||||
|
|
@ -13,6 +13,14 @@ extends Control
|
||||||
# before the menu can act on them, which is the bug that made the formation
|
# before the menu can act on them, which is the bug that made the formation
|
||||||
# editor's drag silently do nothing. Everything in this menu must stay
|
# editor's drag silently do nothing. Everything in this menu must stay
|
||||||
# mouse_filter = IGNORE.
|
# mouse_filter = IGNORE.
|
||||||
|
#
|
||||||
|
# #93: a mouse is a first-class way to drive this menu, not something
|
||||||
|
# emulation turns into touches. The project used to set
|
||||||
|
# emulate_touch_from_mouse, which made every click arrive here as an
|
||||||
|
# InputEventScreenTouch for free; with that off, a menu that only reads
|
||||||
|
# touch events is simply dead under a mouse - which is what the report was.
|
||||||
|
# So the press and the pan are the behaviour, and touch and mouse are two
|
||||||
|
# ways in to them.
|
||||||
|
|
||||||
signal node_picked(id: String)
|
signal node_picked(id: String)
|
||||||
|
|
||||||
|
|
@ -29,6 +37,9 @@ const LOCKED_PULSE: float = 0.5
|
||||||
|
|
||||||
@export var node_radius: float = 46.0
|
@export var node_radius: float = 46.0
|
||||||
@export var row_spacing: float = 250.0
|
@export var row_spacing: float = 250.0
|
||||||
|
# One wheel notch: about a third of the default row spacing, so a notch is a
|
||||||
|
# clear step without skipping a whole depth of the tree.
|
||||||
|
const WHEEL_STEP: float = 90.0
|
||||||
|
|
||||||
# #83: a second tap on the node already selected buys it. The separate buy
|
# #83: a second tap on the node already selected buys it. The separate buy
|
||||||
# button is gone - it sat at the bottom of the panel, a long way from the node
|
# button is gone - it sat at the bottom of the panel, a long way from the node
|
||||||
|
|
@ -93,13 +104,43 @@ func _state_color(node: Dictionary) -> Color:
|
||||||
# Returns true when the press landed on a node, so the caller knows the
|
# Returns true when the press landed on a node, so the caller knows the
|
||||||
# touch was consumed.
|
# touch was consumed.
|
||||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||||
|
return handle_press(event.pressed, event.position)
|
||||||
|
|
||||||
|
|
||||||
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
||||||
|
return handle_pan(event.relative)
|
||||||
|
|
||||||
|
|
||||||
|
# A left click is the same gesture as a tap. Other buttons are left alone so
|
||||||
|
# they stay available for whatever else wants them.
|
||||||
|
func handle_mouse_button(event: InputEventMouseButton) -> bool:
|
||||||
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
|
||||||
|
return handle_pan(Vector2(0.0, WHEEL_STEP))
|
||||||
|
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
|
||||||
|
return handle_pan(Vector2(0.0, -WHEEL_STEP))
|
||||||
|
if event.button_index != MOUSE_BUTTON_LEFT:
|
||||||
|
return false
|
||||||
|
return handle_press(event.pressed, event.position)
|
||||||
|
|
||||||
|
|
||||||
|
# Only while the button is held. A touch drag cannot arrive without a finger
|
||||||
|
# on the glass, but the mouse moves across the menu the whole time it is
|
||||||
|
# open, and panning the tree under a hovering pointer would make the menu
|
||||||
|
# impossible to read.
|
||||||
|
func handle_mouse_motion(event: InputEventMouseMotion) -> bool:
|
||||||
|
if not _dragging:
|
||||||
|
return false
|
||||||
|
return handle_pan(event.relative)
|
||||||
|
|
||||||
|
|
||||||
|
func handle_press(pressed: bool, at: Vector2) -> bool:
|
||||||
if not visible:
|
if not visible:
|
||||||
return false
|
return false
|
||||||
if not event.pressed:
|
if not pressed:
|
||||||
_dragging = false
|
_dragging = false
|
||||||
return true
|
return true
|
||||||
_dragging = true
|
_dragging = true
|
||||||
var local: Vector2 = event.position - global_position
|
var local: Vector2 = at - global_position
|
||||||
for node in GameState.SKILL_TREE:
|
for node in GameState.SKILL_TREE:
|
||||||
if local.distance_to(node_screen_pos(node)) <= node_radius * 1.25:
|
if local.distance_to(node_screen_pos(node)) <= node_radius * 1.25:
|
||||||
var confirming: bool = selected_id == node.id
|
var confirming: bool = selected_id == node.id
|
||||||
|
|
@ -112,12 +153,12 @@ func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
func handle_drag(event: InputEventScreenDrag) -> bool:
|
func handle_pan(relative: Vector2) -> bool:
|
||||||
if not visible:
|
if not visible:
|
||||||
return false
|
return false
|
||||||
# Dragging a finger upward reveals higher research; dragging downward
|
# Dragging a finger upward reveals higher research; dragging downward
|
||||||
# returns to the root. Clamp hard so the canvas never leaves an empty gap.
|
# returns to the root. Clamp hard so the canvas never leaves an empty gap.
|
||||||
scroll_y = clampf(scroll_y - event.relative.y, 0.0, _max_scroll())
|
scroll_y = clampf(scroll_y - relative.y, 0.0, _max_scroll())
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2087,7 +2087,7 @@ anchor_bottom = 1.0
|
||||||
offset_left = 24.0
|
offset_left = 24.0
|
||||||
offset_top = 296.0
|
offset_top = 296.0
|
||||||
offset_right = -24.0
|
offset_right = -24.0
|
||||||
offset_bottom = -434.0
|
offset_bottom = -280.0
|
||||||
script = ExtResource("21")
|
script = ExtResource("21")
|
||||||
|
|
||||||
[node name="DetailLabel" type="Label" parent="UI/SkillMenu"]
|
[node name="DetailLabel" type="Label" parent="UI/SkillMenu"]
|
||||||
|
|
@ -2096,9 +2096,9 @@ anchor_right = 1.0
|
||||||
anchor_top = 1.0
|
anchor_top = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = 32.0
|
offset_left = 32.0
|
||||||
offset_top = -426.0
|
offset_top = -272.0
|
||||||
offset_right = -32.0
|
offset_right = -32.0
|
||||||
offset_bottom = -306.0
|
offset_bottom = -152.0
|
||||||
theme_override_font_sizes/font_size = 34
|
theme_override_font_sizes/font_size = 34
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
@ -2111,9 +2111,9 @@ anchor_top = 1.0
|
||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = -64.0
|
offset_left = -64.0
|
||||||
offset_top = -300.0
|
offset_top = -146.0
|
||||||
offset_right = 76.0
|
offset_right = 76.0
|
||||||
offset_bottom = -236.0
|
offset_bottom = -82.0
|
||||||
script = ExtResource("19")
|
script = ExtResource("19")
|
||||||
digit_height = 52.0
|
digit_height = 52.0
|
||||||
|
|
||||||
|
|
@ -2123,9 +2123,9 @@ anchor_right = 1.0
|
||||||
anchor_top = 1.0
|
anchor_top = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_left = 32.0
|
offset_left = 32.0
|
||||||
offset_top = -230.0
|
offset_top = -76.0
|
||||||
offset_right = -32.0
|
offset_right = -32.0
|
||||||
offset_bottom = -178.0
|
offset_bottom = -24.0
|
||||||
theme_override_font_sizes/font_size = 32
|
theme_override_font_sizes/font_size = 32
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue