From 13492a52e2d87995c542c78a7de53b225372792d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Wisniewski?= Date: Thu, 13 Aug 2026 16:25:41 +0200 Subject: [PATCH] Make research tree vertically scrollable --- BuildController.gd | 3 ++ GameState.gd | 10 +++--- Main.tscn | 1 + SkillTreeView.gd | 50 ++++++++++++++++++++++++----- tests/test_hangar_management.gd.uid | 1 + tests/test_skill_tree.gd | 18 +++++++++++ 6 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 tests/test_hangar_management.gd.uid diff --git a/BuildController.gd b/BuildController.gd index 7c6d3b1..15f8dbc 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -282,6 +282,8 @@ func _unhandled_input(event: InputEvent) -> void: if skill_menu.visible: if event is InputEventScreenTouch: skill_tree.handle_touch(event) + elif event is InputEventScreenDrag: + skill_tree.handle_drag(event) # 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 # fleet and pinch-zoomed the world behind the menu, because @@ -409,6 +411,7 @@ func _on_action_pressed() -> void: if current_building.building_type == GameState.BUILDING_MOTHERSHIP: skill_menu.visible = true + skill_tree.reset_scroll() _refresh_skill_menu() elif current_building.building_type == GameState.BUILDING_SHIP_CRAFTER: # Remembered explicitly: _process() clears current_building while any diff --git a/GameState.gd b/GameState.gd index d56aa12..f19e4b7 100644 --- a/GameState.gd +++ b/GameState.gd @@ -157,7 +157,7 @@ const SKILL_TREE: Array = [ }, { "id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards", - "x": 0.82, "unlocks": BUILDING_COMMAND_CENTER, + "x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "desc": "Build command centers, where the fleet formation is arranged.", }, { @@ -167,12 +167,12 @@ const SKILL_TREE: Array = [ }, { "id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards", - "x": 0.62, "unlocks": BUILDING_SHIP_LAB, + "x": 0.82, "unlocks": BUILDING_SHIP_LAB, "desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.", }, { "id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight", - "x": 0.05, "hub_bonus": 1, + "x": 0.18, "hub_bonus": 1, "desc": "Permanent: +1 transporter stationed at every hub, including ones already built.", }, { @@ -182,12 +182,12 @@ const SKILL_TREE: Array = [ }, { "id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing", - "x": 0.65, "fleet_bonus": 1, + "x": 0.50, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron", - "x": 0.3, "fleet_bonus": 1, + "x": 0.50, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, ] diff --git a/Main.tscn b/Main.tscn index c5749f0..ee36119 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1556,6 +1556,7 @@ script = ExtResource("24") [node name="Tree" type="Control" parent="UI/SkillMenu"] mouse_filter = 2 +clip_contents = true anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 24.0 diff --git a/SkillTreeView.gd b/SkillTreeView.gd index 84def69..b6ea6dc 100644 --- a/SkillTreeView.gd +++ b/SkillTreeView.gd @@ -28,25 +28,45 @@ const LOCKED_CORE: Color = Color(0.17, 0.19, 0.26) const LOCKED_PULSE: float = 0.5 @export var node_radius: float = 46.0 +@export var row_spacing: float = 250.0 var selected_id: String = "" +var scroll_y: float = 0.0 +var _dragging: bool = false + + +func _content_height() -> float: + # The canvas deliberately exceeds the viewport. Four depths at this + # spacing leave clear breathing room around 92px-wide nodes and make each + # research branch read as a route rising away from Shipyards. + return maxf(size.y, GameState.max_skill_depth() * row_spacing + node_radius * 3.0) + + +func _max_scroll() -> float: + return maxf(0.0, _content_height() - size.y) func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE + call_deferred("reset_scroll") + + +func reset_scroll() -> void: + # Start at Shipyards, the base of the tree, whenever the menu opens. + # The player then swipes up to travel along the branches. + scroll_y = _max_scroll() + queue_redraw() func node_screen_pos(node: Dictionary) -> Vector2: - # Height comes from dependency depth, not from the data: the root sits at - # the bottom and each further prerequisite climbs a row, so the tree grows - # upwards as it is researched and stays correct when nodes are added. - var deepest: int = maxi(GameState.max_skill_depth(), 1) - var climb: float = float(GameState.skill_depth(node.id)) / float(deepest) - # Inset so a node on row 0 or column 0 is not clipped in half. + # Every dependency depth gets its own widely-spaced horizontal row. The + # x lane is stable within a branch (set beside SKILL_TREE), so Logistics, + # Freight and Freight Yards rise vertically instead of zig-zagging across + # the display. scroll_y exposes the part of this tall canvas in view. var margin: float = node_radius * 1.4 return Vector2( lerpf(margin, size.x - margin, float(node.x)), - lerpf(size.y - margin, margin, climb) + _content_height() - margin - float(GameState.skill_depth(node.id)) * row_spacing - scroll_y ) @@ -61,8 +81,12 @@ func _state_color(node: Dictionary) -> Color: # Returns true when the press landed on a node, so the caller knows the # touch was consumed. func handle_touch(event: InputEventScreenTouch) -> bool: - if not event.pressed or not visible: + if not visible: return false + if not event.pressed: + _dragging = false + return true + _dragging = true var local: Vector2 = event.position - global_position for node in GameState.SKILL_TREE: if local.distance_to(node_screen_pos(node)) <= node_radius * 1.25: @@ -73,6 +97,16 @@ func handle_touch(event: InputEventScreenTouch) -> bool: return false +func handle_drag(event: InputEventScreenDrag) -> bool: + if not visible: + return false + # Dragging a finger upward reveals higher research; dragging downward + # 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()) + queue_redraw() + return true + + func _draw() -> void: # Links first, so nodes sit on top of them. for node in GameState.SKILL_TREE: diff --git a/tests/test_hangar_management.gd.uid b/tests/test_hangar_management.gd.uid new file mode 100644 index 0000000..4eb74c0 --- /dev/null +++ b/tests/test_hangar_management.gd.uid @@ -0,0 +1 @@ +uid://xc7cie5ogmtp diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index ca9b7b0..7b2a887 100644 --- a/tests/test_skill_tree.gd +++ b/tests/test_skill_tree.gd @@ -105,6 +105,24 @@ func _process(delta: float) -> bool: print("TEST_SKILL_TREE: FAIL (mothership has no research action)") quit(1) return true + # The tree is a tall, vertically scrolling canvas. A branch keeps a + # stable lane as it rises, and dragging exposes higher depths. + var freight: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight")) + var yards: Vector2 = controller.skill_tree.node_screen_pos(game_state.skill_node("freight_yards")) + if absf(freight.x - yards.x) > 1.0 or yards.y >= freight.y: + print("TEST_SKILL_TREE: FAIL (freight branch does not rise vertically)") + quit(1) + return true + var before_scroll: float = controller.skill_tree.scroll_y + var drag: InputEventScreenDrag = InputEventScreenDrag.new() + # Start at the root at the bottom; pull the canvas down to reveal + # the branches rising above it. + drag.relative = Vector2(0.0, 180.0) + controller.skill_tree.handle_drag(drag) + if controller.skill_tree.scroll_y >= before_scroll: + print("TEST_SKILL_TREE: FAIL (tree did not scroll upward)") + quit(1) + return true # --- prerequisites -------------------------------------------- if game_state.can_unlock("wing"):