Make research tree vertically scrollable

This commit is contained in:
Loïc Wisniewski 2026-08-13 16:25:41 +02:00
commit 13492a52e2
6 changed files with 70 additions and 13 deletions

View file

@ -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: