diff --git a/ui/IconButton.gd b/ui/IconButton.gd index 0cf165f..98ae7e7 100644 --- a/ui/IconButton.gd +++ b/ui/IconButton.gd @@ -55,6 +55,8 @@ enum Icon { OUTPOST, # #106: appended, same rule. PLANNING_CENTER, + # #116: the workshop and the drone it sends out. Appended, same rule. + WORKSHOP, REPAIR_DRONE, } # Size the glyphs were originally drawn against. Every glyph dimension is @@ -67,6 +69,11 @@ const REFERENCE_SIZE: float = 56.0 # Selection is an additional frame, never a recolour. It lets menus retain # semantic colours (vessel class, danger, cargo) while still making the # active choice unmistakable. +# #112: the glyph without the button around it. The skill tree draws its own +# hexagon for each node and wants only what is inside one, and a second frame +# nested in that hexagon reads as a button sitting on a node rather than as +# the node's own mark. +@export var show_frame: bool = true @export var highlighted: bool = false: set = set_highlighted # Colour of the bright core stroke. Defaults to near-white, which reads well # on the dark space background; scenes drawing over a bright backdrop (the @@ -206,8 +213,10 @@ func _draw() -> void: var pulse: float = 1.0 if pressed_down else 0.85 var margin: float = 4.0 * s - NeonDraw.draw_glowing_polygon(self, _frame_points(margin), _active_color(), pulse, core_color) - if highlighted: + if show_frame: + NeonDraw.draw_glowing_polygon(self, _frame_points(margin), _active_color(), + pulse, core_color) + if highlighted and show_frame: # The inset frame reads as a selection focus ring. It deliberately uses # the bright core rather than the button's glow colour, so selecting a # blue shield or pink transporter does not change what that colour means. @@ -300,6 +309,10 @@ func _draw() -> void: _draw_outpost_icon(center, pulse, s) Icon.PLANNING_CENTER: _draw_planning_icon(center, pulse, s) + Icon.WORKSHOP: + _draw_workshop_icon(center, pulse, s) + Icon.REPAIR_DRONE: + _draw_repair_drone_icon(center, pulse, s) func _draw_close(center: Vector2, pulse: float, s: float) -> void: @@ -668,6 +681,40 @@ func _draw_ship_lab_icon(center: Vector2, pulse: float, s: float) -> void: _active_color(), pulse, 0.35 * s, core_color) +func _draw_workshop_icon(center: Vector2, pulse: float, s: float) -> void: + # A wrench crossed with a girder. Nothing else in the set is a tool, and + # the meaning wanted is "things are mended here" - the crafter's cradle + # already owns "things are made here". + var arm: float = 11.0 * s + NeonDraw.draw_glowing_line(self, center + Vector2(-arm, arm), + center + Vector2(arm * 0.5, -arm * 0.5), _active_color(), pulse, 0.5 * s, core_color) + # The jaws. + NeonDraw.draw_glowing_path(self, PackedVector2Array([ + center + Vector2(arm * 0.2, -arm * 0.95), + center + Vector2(arm * 0.95, -arm * 0.8), + center + Vector2(arm * 0.8, -arm * 0.1), + center + Vector2(arm * 0.15, -arm * 0.3), + ]), _active_color(), pulse, 0.4 * s, core_color) + # The girder it is working on. + NeonDraw.draw_glowing_line(self, center + Vector2(-arm, -arm * 0.35), + center + Vector2(arm * 0.1, arm), _active_color(), pulse * 0.7, 0.35 * s, core_color) + + +func _draw_repair_drone_icon(center: Vector2, pulse: float, s: float) -> void: + # A small hull with a tool arm out in front, so it reads as a tender + # rather than as another fighter. + var r: float = 9.0 * s + NeonDraw.draw_glowing_polygon(self, PackedVector2Array([ + center + Vector2(0.0, -r), center + Vector2(r * 0.9, 0.0), + center + Vector2(r * 0.6, r), center + Vector2(-r * 0.6, r), + center + Vector2(-r * 0.9, 0.0), + ]), _active_color(), pulse, core_color) + NeonDraw.draw_glowing_line(self, center + Vector2(0.0, -r), + center + Vector2(0.0, -r * 1.9), _active_color(), pulse, 0.35 * s, core_color) + NeonDraw.draw_glowing_line(self, center + Vector2(-r * 0.5, -r * 1.9), + center + Vector2(r * 0.5, -r * 1.9), _active_color(), pulse, 0.35 * s, core_color) + + func _draw_planning_icon(center: Vector2, pulse: float, s: float) -> void: # A tally board: a frame with three bars of different lengths inside it. # Nothing else in the set is rectilinear and horizontal, and the meaning diff --git a/ui/SkillTreeView.gd b/ui/SkillTreeView.gd index 2f4230f..49951c6 100644 --- a/ui/SkillTreeView.gd +++ b/ui/SkillTreeView.gd @@ -69,8 +69,48 @@ func _max_scroll() -> float: return maxf(0.0, _content_height() - size.y) +# #112: one glyph per node, as a child IconButton with its frame turned off. +# Drawn as nodes rather than into _draw() because the glyphs already exist +# there and are non-trivial - reimplementing a dozen of them here would be +# two places to change every time one is redrawn. They are inert: +# MOUSE_FILTER_IGNORE means the tree's own raw-input handling still sees +# every press, exactly as it did when there was nothing on top of the nodes. +var _glyphs: Dictionary = {} + + +func _build_glyphs() -> void: + for node in GameState.SKILL_TREE: + var icon: int = GameState.skill_node_icon(node) + if icon < 0: + continue + var glyph := IconButton.new() + glyph.icon = icon + glyph.show_frame = false + glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(glyph) + _glyphs[String(node.id)] = glyph + + +func _layout_glyphs() -> void: + var span: float = node_radius * 1.5 + for node in GameState.SKILL_TREE: + var glyph: Control = _glyphs.get(String(node.id)) + if glyph == null: + continue + var centre: Vector2 = node_screen_pos(node) + glyph.position = centre - Vector2(span, span) * 0.5 + glyph.size = Vector2(span, span) + # Off the top or bottom of the panel entirely: hidden rather than + # clipped, so a glyph never bleeds past the canvas the tree scrolls + # inside. + glyph.visible = centre.y > -span and centre.y < size.y + span + glyph.glow_color = _state_color(node) + glyph.queue_redraw() + + func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE + _build_glyphs() call_deferred("reset_scroll") @@ -164,6 +204,7 @@ func handle_pan(relative: Vector2) -> bool: func _draw() -> void: + _layout_glyphs() # Links first, so nodes sit on top of them. for node in GameState.SKILL_TREE: if node.requires == "":