Three play-testing reports. 1. Buttons behind an open panel were still clickable. There are two input paths and both leaked. GUI clicks: the research and formation panels cannot block with mouse_filter = STOP, because their contents are driven from raw _unhandled_input and a Control consuming touches through the GUI system swallows them first - so the panels stay transparent and IconButton now checks whether it is behind one, via a "modal_menus" group. Raw input: a touch landing on the research panel but missing a node was left unhandled, so it fell through to GameController - which sits on the ROOT node and sees whatever its children ignore - and moved the fleet or pinch-zoomed the world. 2. "Store ore here" looked dead. It was working: the confirmation was overwritten one frame later by the per-frame status refresh added in #16, so it flashed for a single frame. Craft-panel messages now outlive a refresh (3s), and the button stays ENABLED with an empty hold so pressing it can say why - a greyed button that cannot explain itself is the same dead-button problem. 3. The mothership's description could overlap the button below it. The description block was positioned from the TOP while the buttons were anchored to the BOTTOM, so the gap between them was whatever the screen height left over - overlapping at 1280 and separating at 960. It is one bottom-anchored stack now, checked at both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
336 lines
14 KiB
GDScript
336 lines
14 KiB
GDScript
class_name IconButton
|
|
extends Control
|
|
|
|
# Square neon-outlined button, hand-drawn to match the game's existing
|
|
# no-texture glowing look (same NeonDraw technique as ships/enemies) -
|
|
# no icon image assets, just a glyph drawn inside the frame.
|
|
|
|
signal pressed
|
|
|
|
# Scenes store `icon` as a plain int, so NEW ENTRIES MUST BE APPENDED -
|
|
# inserting one would silently repoint every button after it.
|
|
enum Icon {
|
|
MAP, CLOSE, SHIP, DOWN_ARROW, UP_ARROW, BUILD, ROTATE_CW, ROTATE_CCW,
|
|
# One per building, each a miniature of what actually gets placed. The
|
|
# build menu used to pick from the generic set, which left the mothership
|
|
# and the command centre sharing a folded-map glyph and neither of them
|
|
# looking anything like the structure they build.
|
|
MOTHERSHIP, CRAFTER, COMMAND_CENTER, EXTRACTOR, TRANSPORT_HUB,
|
|
}
|
|
|
|
# Size the glyphs were originally drawn against. Every glyph dimension is
|
|
# scaled by (actual size / this), so the button can be resized in the scene
|
|
# without the icon inside it staying small.
|
|
const REFERENCE_SIZE: float = 56.0
|
|
|
|
@export var icon: int = Icon.MAP
|
|
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
|
# 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
|
|
# lunar surface) override this with a dark colour so the button doesn't wash
|
|
# out. See NeonDraw.
|
|
@export var core_color: Color = Color(1.0, 1.0, 1.0)
|
|
# When false the button is drawn greyed out, but STILL emits `pressed` -
|
|
# the caller decides what to do, which is what lets an unavailable option
|
|
# explain itself instead of silently doing nothing.
|
|
@export var enabled: bool = true: set = set_enabled
|
|
@export var disabled_color: Color = Color(0.42, 0.45, 0.5)
|
|
|
|
var pressed_down: bool = false
|
|
|
|
|
|
func set_enabled(value: bool) -> void:
|
|
if enabled == value:
|
|
return
|
|
enabled = value
|
|
queue_redraw()
|
|
|
|
|
|
func _ready() -> void:
|
|
queue_redraw()
|
|
|
|
|
|
func _blocked_by_modal() -> bool:
|
|
# A panel is open and this button is not part of it, so it is sitting
|
|
# BEHIND the panel and must not react.
|
|
#
|
|
# The panels cannot simply swallow GUI input with a mouse_filter of STOP:
|
|
# the skill tree and the formation editor are driven from raw
|
|
# _unhandled_input (see SkillTreeView), and a Control that consumes
|
|
# touches through the GUI system swallows them before they ever get
|
|
# there. So the panels stay transparent to input and the buttons check
|
|
# instead.
|
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
|
if menu.visible and not menu.is_ancestor_of(self):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _gui_input(event: InputEvent) -> void:
|
|
if _blocked_by_modal():
|
|
return
|
|
if event is InputEventScreenTouch:
|
|
_handle_press(event.pressed, event.position)
|
|
accept_event()
|
|
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
_handle_press(event.pressed, event.position)
|
|
accept_event()
|
|
|
|
|
|
func _handle_press(is_pressed: bool, at_position: Vector2) -> void:
|
|
if is_pressed:
|
|
pressed_down = true
|
|
queue_redraw()
|
|
elif pressed_down:
|
|
pressed_down = false
|
|
queue_redraw()
|
|
# Only counts as a real press if the finger/cursor is still over
|
|
# the button when it lifts - dragging off before releasing (a
|
|
# common way to "cancel" a touch) must not still trigger it.
|
|
if Rect2(Vector2.ZERO, size).has_point(at_position):
|
|
pressed.emit()
|
|
|
|
|
|
func _active_color() -> Color:
|
|
return glow_color if enabled else disabled_color
|
|
|
|
|
|
func _draw() -> void:
|
|
var rect_size: Vector2 = size
|
|
var s: float = min(rect_size.x, rect_size.y) / REFERENCE_SIZE
|
|
# Constant glow, brighter while held. Only the press state changes it,
|
|
# so the button redraws on press/release instead of every frame.
|
|
var pulse: float = 1.0 if pressed_down else 0.85
|
|
|
|
var margin: float = 4.0 * s
|
|
var square: PackedVector2Array = PackedVector2Array([
|
|
Vector2(margin, margin),
|
|
Vector2(rect_size.x - margin, margin),
|
|
Vector2(rect_size.x - margin, rect_size.y - margin),
|
|
Vector2(margin, rect_size.y - margin),
|
|
])
|
|
NeonDraw.draw_glowing_polygon(self, square, _active_color(), pulse, core_color)
|
|
|
|
var center: Vector2 = rect_size / 2.0
|
|
match icon:
|
|
Icon.CLOSE:
|
|
_draw_close(center, pulse, s)
|
|
Icon.SHIP:
|
|
_draw_ship(center, pulse, s)
|
|
Icon.MAP:
|
|
_draw_map(center, pulse, s)
|
|
Icon.DOWN_ARROW:
|
|
_draw_arrow(center, pulse, 1.0, s)
|
|
Icon.UP_ARROW:
|
|
_draw_arrow(center, pulse, -1.0, s)
|
|
Icon.BUILD:
|
|
_draw_build(center, pulse, s)
|
|
Icon.ROTATE_CW:
|
|
_draw_rotate(center, pulse, s, 1.0)
|
|
Icon.ROTATE_CCW:
|
|
_draw_rotate(center, pulse, s, -1.0)
|
|
Icon.MOTHERSHIP:
|
|
_draw_mothership_icon(center, pulse, s)
|
|
Icon.CRAFTER:
|
|
_draw_crafter_icon(center, pulse, s)
|
|
Icon.COMMAND_CENTER:
|
|
_draw_command_icon(center, pulse, s)
|
|
Icon.EXTRACTOR:
|
|
_draw_extractor_icon(center, pulse, s)
|
|
Icon.TRANSPORT_HUB:
|
|
_draw_transport_hub_icon(center, pulse, s)
|
|
|
|
|
|
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
|
var r: float = 10.0 * s
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-r, -r), center + Vector2(r, r), _active_color(), pulse, 0.5 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-r, r), center + Vector2(r, -r), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
|
func _draw_ship(center: Vector2, pulse: float, s: float) -> void:
|
|
var nose: Vector2 = center + Vector2(0, -11) * s
|
|
var left: Vector2 = center + Vector2(-7, 8) * s
|
|
var right: Vector2 = center + Vector2(7, 8) * s
|
|
var points: PackedVector2Array = PackedVector2Array([nose, left, right])
|
|
NeonDraw.draw_glowing_polygon(self, points, _active_color(), pulse, core_color)
|
|
|
|
|
|
func _draw_arrow(center: Vector2, pulse: float, direction: float, s: float) -> void:
|
|
var r: float = 9.0 * s
|
|
var tip: Vector2 = center + Vector2(0, r * direction)
|
|
var shaft_top: Vector2 = center + Vector2(0, -r * direction)
|
|
NeonDraw.draw_glowing_line(self, shaft_top, tip, _active_color(), pulse, 0.5 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(-r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, tip, tip + Vector2(r * 0.7, -r * 0.8 * direction), _active_color(), pulse, 0.5 * s, core_color)
|
|
|
|
|
|
func _draw_build(center: Vector2, pulse: float, s: float) -> void:
|
|
# A plus inside a frame - "add a structure".
|
|
var r: float = 11.0 * s
|
|
var frame: PackedVector2Array = PackedVector2Array([
|
|
center + Vector2(-r, -r), center + Vector2(r, -r),
|
|
center + Vector2(r, r), center + Vector2(-r, r),
|
|
])
|
|
NeonDraw.draw_glowing_polygon(self, frame, _active_color(), pulse, core_color)
|
|
var arm: float = r * 0.55
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-arm, 0), center + Vector2(arm, 0), _active_color(), pulse, 0.4 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -arm), center + Vector2(0, arm), _active_color(), pulse, 0.4 * s, core_color)
|
|
|
|
|
|
func _draw_rotate(center: Vector2, pulse: float, s: float, direction: float) -> void:
|
|
# Three-quarter arc with an arrowhead on the leading end, mirrored for
|
|
# the counter-clockwise variant.
|
|
var r: float = 11.0 * s
|
|
var segments: int = 14
|
|
var sweep: float = TAU * 0.75
|
|
var start: float = -PI * 0.5
|
|
var arc: PackedVector2Array = PackedVector2Array()
|
|
for i in range(segments + 1):
|
|
var t: float = float(i) / segments
|
|
var a: float = start + sweep * t * direction
|
|
arc.append(center + Vector2(cos(a), sin(a)) * r)
|
|
for i in range(segments):
|
|
NeonDraw.draw_glowing_line(self, arc[i], arc[i + 1], _active_color(), pulse, 0.34 * s, core_color)
|
|
|
|
var tip: Vector2 = arc[arc.size() - 1]
|
|
var tangent: Vector2 = (tip - arc[arc.size() - 2]).normalized()
|
|
var normal: Vector2 = Vector2(-tangent.y, tangent.x)
|
|
var head: float = 5.0 * s
|
|
NeonDraw.draw_glowing_line(self, tip, tip - tangent * head + normal * head, _active_color(), pulse, 0.34 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, tip, tip - tangent * head - normal * head, _active_color(), pulse, 0.34 * s, core_color)
|
|
|
|
|
|
# --- building glyphs, each echoing the placed structure's silhouette ------
|
|
|
|
func _draw_mothership_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
# Radial flagship: a reactor core with six swept talons around it, the
|
|
# same silhouette as the placed structure.
|
|
for i in range(6):
|
|
var base: float = (float(i) / 6.0) * TAU
|
|
var talon: PackedVector2Array = PackedVector2Array()
|
|
for step in [0.0, 0.5, 1.0]:
|
|
var a: float = base + 0.72 * pow(step, 1.3)
|
|
talon.append(center + Vector2(cos(a), sin(a)) * lerpf(7.0, 16.0, step) * s)
|
|
for step in [1.0, 0.5, 0.0]:
|
|
var a: float = base + 0.72 * pow(step, 1.3) - (0.34 * (1.0 - step) + 0.05)
|
|
talon.append(center + Vector2(cos(a), sin(a)) * lerpf(7.0, 16.0, step) * s)
|
|
NeonDraw.draw_glowing_polygon(self, talon, _active_color(), pulse, core_color)
|
|
|
|
for radius in [6.5, 3.0]:
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
for i in range(6):
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
ring.append(center + Vector2(cos(a), sin(a)) * radius * s)
|
|
NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color)
|
|
|
|
|
|
func _draw_transport_hub_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
# A ring of docking cradles with a freight box in the middle: reads as
|
|
# "things are stationed here and go out from here", which is what the hub
|
|
# does, and shares no silhouette with the crafter's open cradle.
|
|
var ring_r: float = 11.0 * s
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
for i in range(8):
|
|
var a: float = (float(i) / 8.0) * TAU + PI / 8.0
|
|
ring.append(center + Vector2(cos(a), sin(a)) * ring_r)
|
|
NeonDraw.draw_glowing_polygon(self, ring, _active_color(), pulse, core_color)
|
|
|
|
var box: float = 4.2 * s
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
center + Vector2(-box, -box), center + Vector2(box, -box),
|
|
center + Vector2(box, box), center + Vector2(-box, box),
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
# Three stubby berths, so the ring reads as occupied rather than as a
|
|
# plain octagon.
|
|
for i in range(3):
|
|
var a: float = (float(i) / 3.0) * TAU - PI / 2.0
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
NeonDraw.draw_glowing_line(self, center + dir * (box * 1.5),
|
|
center + dir * (ring_r - 1.5 * s), _active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
|
|
func _draw_crafter_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
# Reactor core, truss spine, and an open cradle holding a hull.
|
|
var core_centre: Vector2 = center + Vector2(-11, 0) * s
|
|
var core: PackedVector2Array = PackedVector2Array()
|
|
for i in range(6):
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
core.append(core_centre + Vector2(cos(a), sin(a)) * 5.0 * s)
|
|
NeonDraw.draw_glowing_polygon(self, core, _active_color(), pulse, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-6, 0) * s,
|
|
center + Vector2(-2, 0) * s, _active_color(), pulse, 0.35 * s, core_color)
|
|
|
|
var bay: Vector2 = center + Vector2(6, 0) * s
|
|
var cradle: PackedVector2Array = PackedVector2Array()
|
|
for i in range(13):
|
|
var a: float = lerpf(-0.25 * PI, 1.25 * PI, float(i) / 12.0)
|
|
cradle.append(bay + Vector2(cos(a), sin(a)) * 10.0 * s)
|
|
NeonDraw.draw_glowing_path(self, cradle, _active_color(), pulse, 0.4 * s, core_color)
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
bay + Vector2(0, -6) * s, bay + Vector2(-5, 4) * s, bay + Vector2(5, 4) * s,
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
func _draw_command_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
# Hex plotting rim with spokes to a diamond hub, echoing the placed
|
|
# structure's silhouette: half the ports carry a formation token.
|
|
var hex_r: float = 11.0 * s
|
|
var hub_r: float = 4.0 * s
|
|
var hex: PackedVector2Array = PackedVector2Array()
|
|
for i in range(6):
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
hex.append(center + Vector2(cos(a), sin(a)) * hex_r)
|
|
NeonDraw.draw_glowing_polygon(self, hex, _active_color(), pulse, core_color)
|
|
|
|
for i in range(6):
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
NeonDraw.draw_glowing_line(self, center + dir * hub_r, center + dir * hex_r, _active_color(), pulse, 0.32 * s, core_color)
|
|
if i % 2 == 0:
|
|
var tip: Vector2 = center + dir * hex_r
|
|
var t: float = 2.6 * s
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
tip + Vector2(0, -t), tip + Vector2(t, 0), tip + Vector2(0, t), tip + Vector2(-t, 0),
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
|
center + Vector2(0, -hub_r), center + Vector2(hub_r, 0),
|
|
center + Vector2(0, hub_r), center + Vector2(-hub_r, 0),
|
|
]), _active_color(), pulse, core_color)
|
|
|
|
|
|
func _draw_extractor_icon(center: Vector2, pulse: float, s: float) -> void:
|
|
# A derrick standing on a planet's limb, with ore lifting off it - the
|
|
# one building that only works over a planet, so the planet is the point.
|
|
var limb: PackedVector2Array = PackedVector2Array()
|
|
for i in range(11):
|
|
var a: float = lerpf(-0.82 * PI, -0.18 * PI, float(i) / 10.0)
|
|
limb.append(center + (Vector2(0, 22) + Vector2(cos(a), sin(a)) * 17.0) * s)
|
|
NeonDraw.draw_glowing_path(self, limb, _active_color(), pulse, 0.45 * s, core_color)
|
|
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-6, 5) * s, center + Vector2(0, -9) * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(6, 5) * s, center + Vector2(0, -9) * s, _active_color(), pulse, 0.4 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-3.5, -1) * s, center + Vector2(3.5, -1) * s, _active_color(), pulse, 0.35 * s, core_color)
|
|
# Shaft driven down through the limb - without it the derrick reads as a
|
|
# tower standing on a hill rather than as something mining the planet.
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(0, -1) * s, center + Vector2(0, 14) * s, _active_color(), pulse, 0.3 * s, core_color)
|
|
|
|
var ore_centre: Vector2 = center + Vector2(0, -15) * s
|
|
var ore: PackedVector2Array = PackedVector2Array()
|
|
for i in range(6):
|
|
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
|
ore.append(ore_centre + Vector2(cos(a), sin(a)) * 4.0 * s)
|
|
NeonDraw.draw_glowing_polygon(self, ore, _active_color(), pulse, core_color)
|
|
|
|
|
|
func _draw_map(center: Vector2, pulse: float, s: float) -> void:
|
|
var w: float = 11.0 * s
|
|
var h: float = 9.0 * s
|
|
var outline: PackedVector2Array = PackedVector2Array([
|
|
center + Vector2(-w, -h), center + Vector2(w, -h * 0.7),
|
|
center + Vector2(w, h), center + Vector2(-w, h * 0.7),
|
|
])
|
|
NeonDraw.draw_glowing_polygon(self, outline, _active_color(), pulse, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(-w * 0.34, -h * 0.9), center + Vector2(-w * 0.34, h * 0.77), _active_color(), pulse, 0.4 * s, core_color)
|
|
NeonDraw.draw_glowing_line(self, center + Vector2(w * 0.34, -h * 0.77), center + Vector2(w * 0.34, h * 0.9), _active_color(), pulse, 0.4 * s, core_color)
|