#105 shipped with interceptors as the only garrisonable vessel, and the reason was a design fault rather than a decision: firing the laser and the gas bomb lived in the player's fleet controllers, so a stationed laser would have held its position and never shot. The shot is now a thing in the world rather than something a controller draws. LaserBeamEffect and GasBombEffect are self-contained, self-animating and self-freeing, added to the tree root - the same shape EnemyBigLaserBeam and ArtilleryShell already had on the enemy side - and they outlive the vessel that fired them. Above them sits one seam, on VesselCooldownFillComponent: ability_range(), ability_ammo_resource(), ability_ammo_cost() and fire_ability(). A beam and a bomb are now the same shape of thing to whoever pulls the trigger, so the garrison AI never names an ability - the test asserts that literally, by reading the AI's own source. A third armed vessel needs to implement the seam and nothing else. Ammunition is the CALLER's business, deliberately: the fleet spends from its hold, a garrison from its outpost's store, and neither concern belongs in the component. A garrison with an empty magazine holds fire rather than falling back on the fleet's hold. Outpost storage: 50 gas per stationed bomber and 50 energy per stationed laser, off the VESSEL definitions, so an outpost never learns which vessel needs what. It is a capacity, not a stock - hub transporters fill it exactly as they fill a gas burner's buffer, because building_logistics_deficit now takes the larger of the definition's static buffer and the garrison's magazine. Every other resource is capped at zero: an outpost is not a depot, and ore parked at one has nothing to do there and no transporter coming for it. Also restores two properties the Godot editor dropped from GasBurnerBuildingDefinition.tres when it re-saved the file: collected_outputs and capabilities. Without the first, nothing ever collects the burner's energy - test_gas_burner caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
5.8 KiB
GDScript
166 lines
5.8 KiB
GDScript
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()
|
|
|
|
|
|
# --- the ability seam ------------------------------------------------------
|
|
#
|
|
# Everything a caller needs to fire this vessel's ability WITHOUT knowing
|
|
# which ability it is. Both the player's controllers and the outpost garrison
|
|
# AI go through these, so a beam and a bomb are the same shape of thing to
|
|
# whoever pulls the trigger, and adding a third ability means implementing
|
|
# these rather than editing the AI.
|
|
#
|
|
# `ability_ammo_*` describe the magazine, not where it comes from: the fleet
|
|
# spends from its hold, a garrison from its outpost's store, and neither
|
|
# concern belongs down here.
|
|
|
|
func has_ability() -> bool:
|
|
return ability_range() > 0.0
|
|
|
|
|
|
func ability_range() -> float: return 0.0
|
|
func ability_ammo_resource() -> String: return ""
|
|
func ability_ammo_cost() -> int: return 0
|
|
func ability_cooldown() -> float: return cooldown_duration
|
|
|
|
|
|
# Spends the cooldown and puts the effect in the world. Returns false when the
|
|
# ability was not ready - ammunition is the CALLER's business, because only
|
|
# the caller knows which store it is spending from.
|
|
func fire_ability(_world: Node, _origin: Vector2, _at: Vector2) -> bool:
|
|
return false
|
|
|
|
|
|
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)
|