class_name VesselCooldownFillComponent extends Node2D # Shared per-vessel cooldown state and hull-fill feedback for abilities that # grant one independently recharging shot per ship. var cooldown_duration: float = 1.0 var cooldown_left: float = 0.0 # Two vertices this close together are the same vertex as far as the fill is # concerned - a hull is tens of units across, so this is well below anything # that could be a real edge. const WELD_DISTANCE: float = 0.01 # Below this the fill is a sliver nobody can see, and drawing it is all cost # and no picture. const MIN_FILL_AREA: float = 0.5 func setup() -> void: show_behind_parent = true queue_redraw() func is_ready() -> bool: return cooldown_left <= 0.0 func _begin_cooldown(duration: float) -> bool: if not is_ready(): return false cooldown_duration = maxf(duration, 0.001) cooldown_left = cooldown_duration queue_redraw() return true func tick(delta: float) -> void: if cooldown_left <= 0.0: return cooldown_left = maxf(cooldown_left - delta, 0.0) queue_redraw() func cooldown_progress() -> float: return 1.0 - cooldown_left / maxf(cooldown_duration, 0.001) # The part of the hull that is "filled up" at this progress, as a list of # polygons - plural, because a hull with a notch in it (the gas bomber's # tail, the distorder's waist) genuinely fills as two separate puddles # before the level rises past the notch. # # Geometry2D does the clipping. This used to walk the hull itself and cut # each edge against the fill line, which is Sutherland-Hodgman, and # Sutherland-Hodgman is only sound for a CONVEX subject: on a notched hull # it stitches the pieces back together with a zero-width bridge, and a # polygon with a zero-width bridge is not one Godot's triangulator will # accept. It said so, once per frame per cooling vessel, for as long as any # cooldown ran. func _filled_hull(points: PackedVector2Array, progress: float) -> Array: if points.size() < 3 or progress <= 0.001: return [] if progress >= 0.999: return [points.duplicate()] var top: float = INF var bottom: float = -INF var left: float = INF var right: float = -INF for point in points: top = minf(top, point.y) bottom = maxf(bottom, point.y) left = minf(left, point.x) right = maxf(right, point.x) var fill_y: float = lerpf(bottom, top, clampf(progress, 0.0, 1.0)) # Deliberately oversized sideways and downwards, so the box's own edges # never coincide with the hull's and the only cut is the horizontal one. var box := PackedVector2Array([ Vector2(left - 8.0, fill_y), Vector2(right + 8.0, fill_y), Vector2(right + 8.0, bottom + 8.0), Vector2(left - 8.0, bottom + 8.0), ]) var pieces: Array = [] for piece in Geometry2D.intersect_polygons(points, box): var drawable: PackedVector2Array = _drawable(piece) if not drawable.is_empty(): pieces.append(drawable) return pieces # Last guard before the renderer. The clipper is sound where the old walk # was not, but it can still hand back a vertex repeated at a corner, or a # sliver of a puddle at the very start of a fill - neither is worth drawing # and both are the shapes triangulation objects to. func _drawable(points: PackedVector2Array) -> PackedVector2Array: var welded := PackedVector2Array() for point in points: if welded.is_empty() \ or welded[welded.size() - 1].distance_to(point) > WELD_DISTANCE: welded.append(point) # The polygon is implicitly closed, so the last point meeting the first is # the same duplication seen from the other end. while welded.size() >= 2 \ and welded[0].distance_to(welded[welded.size() - 1]) <= WELD_DISTANCE: welded.remove_at(welded.size() - 1) if welded.size() < 3 or _area(welded) < MIN_FILL_AREA: return PackedVector2Array() return welded # Shoelace, unsigned: the clip preserves the hull's winding, but which way a # hull winds is the definition's business and not worth depending on here. func _area(points: PackedVector2Array) -> float: var total: float = 0.0 var previous: Vector2 = points[points.size() - 1] for point in points: total += previous.x * point.y - point.x * previous.y previous = point return absf(total) * 0.5 func _draw() -> void: var ship: Node = get_parent() if ship == null or ship.definition == null: return var hull: PackedVector2Array = ship.definition.hull_points var ready_color: Color = _ready_color() if is_ready(): draw_colored_polygon(hull, Color(ready_color, 0.42)) NeonDraw.draw_glowing_polygon(self, hull, ready_color, 0.75, ship.core_color, 0.75) return draw_colored_polygon(hull, Color(_cooling_color(), 0.58)) for piece in _filled_hull(hull, cooldown_progress()): draw_colored_polygon(piece, Color(ready_color, 0.68)) func _ready_color() -> Color: return Color.WHITE func _cooling_color() -> Color: return Color(0.2, 0.2, 0.2)