extends Node2D # A player-placed structure in space. Three kinds for now: # mothership - where a wiped-out fleet is rebuilt # ship_crafter - fly the fleet onto it to buy additional ships # command_center - reconfigure the fleet formation # # Buildings are stationary and harmless; whether the fleet is "on" one is # decided by BuildingField comparing the fleet CENTROID against # interact_radius, the same stable single-number test Planet uses (see # Planet.gd for why per-ship overlap was a poor fit). # # Nothing here rests on ground or vents into an atmosphere - these are # orbital structures, so they are built out of trusses, radiator fins, # reactor cores and open cradles rather than walls and roofs. # # The crafter and the mothership animate, so they run _process. The command # centre is drawn once and never redrawn - per-frame redraws of art that # never changes were a real framerate cost earlier on. @export var building_type: String = "mothership" # Which GameState.buildings record this node is showing. Set by # BuildingField when it spawns, so a node can be traced back to the stock # and production queue that survive the scene swap (#16). @export var building_id: int = 0 @export var interact_radius: float = 150.0 const MOTHERSHIP_COLOR: Color = Color(0.45, 0.9, 0.55) const CRAFTER_COLOR: Color = Color(1.0, 0.72, 0.25) const COMMAND_COLOR: Color = Color(0.55, 0.6, 1.0) const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85) # Transport hub: an octagonal dock ring with berths around it and a freight # core in the middle. Deliberately radial-but-flat, so it does not read as a # small mothership - the mothership is the only thing with swept arms. const HUB_RING_RADIUS: float = 74.0 const HUB_DECK_RADIUS: float = 54.0 const HUB_CORE_RADIUS: float = 22.0 const HUB_BERTH_COUNT: int = 8 const HUB_BERTH_LENGTH: float = 20.0 const HUB_DOCKED_COUNT: int = 3 const HUB_DOCKED_ORBIT: float = 96.0 const HUB_SPIN_SPEED: float = 0.22 # The vessel on the assembly line is drawn in the fleet's own colour, so it # reads as "one of yours being built" rather than as shipyard machinery. const HULL_COLOR: Color = Color(0.25, 0.85, 1.0) # One assembly cycle: parts are welded on, the finished hull flashes, then # it clears the cradle and the line starts over. const CYCLE: float = 3.6 const ASSEMBLE_END: float = 0.66 # fraction of CYCLE spent welding const FLASH_END: float = 0.76 # ... then the completion flash # ... then launch, to 1.0 const CORE_CENTRE: Vector2 = Vector2(-58.0, 12.0) const BAY_CENTRE: Vector2 = Vector2(46.0, 12.0) const RIB_RADIUS: float = 36.0 const RAIL_Y: float = -32.0 const FLASH_RING_RADIUS: float = 20.0 # The hull in four pieces, attached in this order. Each carries the point # the crane welds it on at, which is where the sparks fly from. const SHIP_PARTS: Array = [ {"at": 0.04, "weld": Vector2(0, -12), "points": [Vector2(0, -26), Vector2(-8, 4), Vector2(8, 4)]}, {"at": 0.28, "weld": Vector2(-13, 7), "points": [Vector2(-8, -4), Vector2(-22, 14), Vector2(-8, 10)]}, {"at": 0.48, "weld": Vector2(13, 7), "points": [Vector2(8, -4), Vector2(22, 14), Vector2(8, 10)]}, {"at": 0.70, "weld": Vector2(0, 10), "points": [Vector2(-7, 4), Vector2(7, 4), Vector2(5, 14), Vector2(-5, 14)]}, ] # The assembly clock, wrapped at CYCLE because the whole build sequence is # expressed as a fraction of it. var anim_time: float = 0.0 # Free-running seconds for everything that is merely cyclic - reactor pulses, # orbiting satellites, blinking beacons. These must NOT share anim_time: # CYCLE is 3.6s, which is not a whole number of periods for any of them, so # wrapping made every one of them leap at the wrap. The satellites were the # obvious casualty, snapping back 66 degrees every 3.6 seconds. # # Deliberately never wrapped. GDScript floats are 64-bit, so even after hours # of play the argument to sin() keeps far more precision than a frame needs, # and any wrap point would reintroduce exactly the discontinuity above. var phase_time: float = 0.0 var _hull_parts: Array[PackedVector2Array] = [] # Every moving element is its own NeonPiece, rasterised once when the # building is created and thereafter only moved, scaled, faded or hidden. # Nothing here ever calls queue_redraw() again: the building's own _draw() # lays down the fixed structure a single time, and the animation is pure # node-property work from then on. var _core_cell: NeonPiece = null var _satellites: Node2D = null var _beacons: Array = [] var _clamps: NeonPiece = null var _crane: Node2D = null var _hoist: NeonPiece = null var _sparks: Node2D = null var _hull_holder: Node2D = null var _hull_pieces: Array = [] # Rotating holder for the transports stationed at a transport hub (#18). var _hub_traffic: Node2D = null var _flash_ring: NeonPiece = null var _wash: NeonPiece = null func _ready() -> void: add_to_group("buildings") z_index = -1 # The crafter runs an assembly line and the mothership holds a live # reactor with satellites in orbit, so both animate. The command centre is # entirely static and needs no per-frame work at all. var animated: bool = building_type != GameState.BUILDING_COMMAND_CENTER set_process(animated) if building_type == GameState.BUILDING_SHIP_CRAFTER: for part in SHIP_PARTS: _hull_parts.append(PackedVector2Array(part.points)) _build_crafter_pieces() elif building_type == GameState.BUILDING_MOTHERSHIP: _build_mothership_pieces() elif building_type == GameState.BUILDING_TRANSPORT_HUB: _build_hub_pieces() queue_redraw() func _process(delta: float) -> void: anim_time = fmod(anim_time + delta, CYCLE) phase_time += delta apply_animation() # Pushes the current clock onto the movable pieces. Normally driven from # _process; public so a preview or a test can pin the clock and see exactly # that frame without having to run the animation up to it in real time. func apply_animation() -> void: match building_type: GameState.BUILDING_MOTHERSHIP: _animate_mothership() GameState.BUILDING_SHIP_CRAFTER: _animate_crafter() GameState.BUILDING_TRANSPORT_HUB: _animate_transport_hub() # Where the satellites are, and how open the reactor core is. Both are read # by _draw and by the test that checks they stay continuous across the # assembly clock's wrap. func orbit_angle() -> float: return phase_time * ORBIT_SPEED func core_pulse() -> float: return 0.5 + 0.5 * sin(phase_time * PULSE_SPEED) func get_display_name() -> String: match building_type: GameState.BUILDING_MOTHERSHIP: return "Mothership" GameState.BUILDING_SHIP_CRAFTER: return "Ship Crafter" _: return "Command Center" # How many times this building's geometry has actually been rasterised. The # whole point of the NeonPiece decomposition is that this stays at 1 for the # building's lifetime however long it animates, so it is worth being able to # assert rather than assume. var draw_count: int = 0 func _draw() -> void: draw_count += 1 # Runs exactly once per building: this is the fixed structure. Everything # that moves is a NeonPiece child, animated through node properties, so # nothing here ever asks for a redraw again. match building_type: GameState.BUILDING_COMMAND_CENTER: _draw_command_center() GameState.BUILDING_MOTHERSHIP: _draw_mothership_static(self) GameState.BUILDING_TRANSPORT_HUB: _draw_transport_hub_static(self) _: _draw_crafter_static(self) # --- shared helpers ------------------------------------------------------- func _ring(centre: Vector2, radius: float, sides: int, phase: float = 0.0) -> PackedVector2Array: var pts: PackedVector2Array = PackedVector2Array() for i in range(sides): var a: float = (float(i) / float(sides)) * TAU + phase pts.append(centre + Vector2(cos(a), sin(a)) * radius) return pts func _arc(centre: Vector2, radius: float, from_a: float, to_a: float, steps: int) -> PackedVector2Array: var pts: PackedVector2Array = PackedVector2Array() for i in range(steps + 1): var a: float = lerpf(from_a, to_a, float(i) / float(steps)) pts.append(centre + Vector2(cos(a), sin(a)) * radius) return pts # --- command centre: a tactical plotting hub ------------------------------ # The mothership is radial-and-swept, the crafter is an asymmetric assembly # line, so the command centre reads best as the odd one out: rigid, hexagonal, # a wheel rather than a claw. Six spokes tie a hex rim to a central diamond # readout, and every spoke ends in a formation port - half of them holding a # token, half left open - so the structure itself shows fleet slots waiting # to be filled, which is exactly what the drag-and-drop editor inside does. const CMD_HEX_RADIUS: float = 88.0 const CMD_DECK_RADIUS: float = 66.0 const CMD_HUB_RADIUS: float = 30.0 const CMD_SCREEN_RADIUS: float = 14.0 const CMD_PORT_COUNT: int = 6 const CMD_PORT_LENGTH: float = 16.0 const CMD_TOKEN_RADIUS: float = 9.0 const CMD_STUD_RADIUS: float = 7.0 # --- transport hub -------------------------------------------------------- func _draw_transport_hub_static(target: CanvasItem) -> void: var c: Color = TRANSPORT_HUB_COLOR # Dock ring and the deck nested inside it. NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_RING_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 1.0) NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_DECK_RADIUS, HUB_BERTH_COUNT, PI / 8.0), c, 0.45, NeonDraw.DEFAULT_CORE, 0.7) # Freight core: a squared-off hold rather than a reactor, so it reads as # storage and not as another power plant. NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, HUB_CORE_RADIUS, 4, PI / 4.0), c, 0.95) for i in range(HUB_BERTH_COUNT): var a: float = (float(i) / HUB_BERTH_COUNT) * TAU + PI / 8.0 var dir: Vector2 = Vector2(cos(a), sin(a)) NeonDraw.draw_glowing_line(target, dir * HUB_CORE_RADIUS, dir * HUB_DECK_RADIUS, c, 0.5, 0.32) # Berth arm standing off the ring, where a transporter parks. NeonDraw.draw_glowing_line(target, dir * HUB_RING_RADIUS, dir * (HUB_RING_RADIUS + HUB_BERTH_LENGTH), c, 0.75, 0.38) func _build_hub_pieces() -> void: # Stationed transports, as NeonPiece children of one holder that turns. # Per #10 the geometry is rasterised once and only the holder's rotation # changes, so the traffic costs no redraws at all. _hub_traffic = Node2D.new() add_child(_hub_traffic) for i in range(HUB_DOCKED_COUNT): var a: float = (float(i) / HUB_DOCKED_COUNT) * TAU var at: Vector2 = Vector2(cos(a), sin(a)) * HUB_DOCKED_ORBIT var piece: NeonPiece = NeonPiece.create(_hub_traffic, at, TRANSPORT_HUB_COLOR) # A stubby freighter: blunt, boxy, nothing like the fleet's darts. piece.add_polygon(PackedVector2Array([ Vector2(-9, -5), Vector2(9, -5), Vector2(12, 0), Vector2(9, 5), Vector2(-9, 5), ])) piece.rotation = a + PI / 2.0 piece.queue_redraw() func _animate_transport_hub() -> void: _hub_traffic.rotation = phase_time * HUB_SPIN_SPEED func _diamond(centre: Vector2, r: float) -> PackedVector2Array: return PackedVector2Array([ centre + Vector2(0, -r), centre + Vector2(r, 0), centre + Vector2(0, r), centre + Vector2(-r, 0), ]) func _draw_command_center() -> void: var c: Color = COMMAND_COLOR var readout: Color = Color(0.82, 0.86, 1.0) # The rim: a hex table holding the whole layout together, with a fainter # deck ring nested just inside it - the same "plating has layers" read the # mothership gets from its nested reactor rings. NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_HEX_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 1.0) NeonDraw.draw_glowing_polygon(self, _ring(Vector2.ZERO, CMD_DECK_RADIUS, CMD_PORT_COUNT, PI / 6.0), c, 0.5, NeonDraw.DEFAULT_CORE, 0.7) for i in range(CMD_PORT_COUNT): var a: float = (float(i) / CMD_PORT_COUNT) * TAU + PI / 6.0 var dir: Vector2 = Vector2(cos(a), sin(a)) # Spoke bracing the hub to the rim. NeonDraw.draw_glowing_line(self, dir * CMD_HUB_RADIUS, dir * CMD_HEX_RADIUS, c, 0.65, 0.36) # Socket where the mast plugs into the rim. NeonDraw.draw_glowing_polygon(self, _ring(dir * CMD_HEX_RADIUS, CMD_STUD_RADIUS, 6, a), c, 0.85) # Port mast standing proud of the rim; every other one is manned. var port_tip: Vector2 = dir * (CMD_HEX_RADIUS + CMD_PORT_LENGTH) NeonDraw.draw_glowing_line(self, dir * CMD_HEX_RADIUS, port_tip, c, 0.8, 0.4) if i % 2 == 0: NeonDraw.draw_glowing_polygon(self, _diamond(port_tip, CMD_TOKEN_RADIUS), c, 1.0) # Command core: a diamond hub with a lit readout set inside it. NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_HUB_RADIUS), c, 1.0) NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9) # --- mothership: a radial flagship ---------------------------------------- # Not a big fighter. A mothership is the thing everything else orbits, so it # is built radially around a reactor core rather than pointed in a direction: # six swept talons, crystal panels set between them, and satellites circling # the whole assembly. A forward-facing hull, however large, just reads as # another ship. const ARM_COUNT: int = 6 const ARM_INNER: float = 42.0 const ARM_OUTER: float = 106.0 const ARM_SWEEP: float = 0.72 const SATELLITE_COUNT: int = 4 const SATELLITE_ORBIT: float = 126.0 const ORBIT_SPEED: float = 0.32 const PULSE_SPEED: float = 2.4 func _talon(base_angle: float) -> PackedVector2Array: # One swept claw: the outer edge runs from the core to the tip, and the # inner edge comes back a little behind it, closing to a point so the arm # tapers instead of ending in a blunt stub. var steps: int = 7 var pts: PackedVector2Array = PackedVector2Array() for i in range(steps + 1): var f: float = float(i) / float(steps) var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) for i in range(steps, -1, -1): var f: float = float(i) / float(steps) var trail: float = 0.34 * (1.0 - f) + 0.04 var a: float = base_angle + ARM_SWEEP * pow(f, 1.3) - trail pts.append(Vector2(cos(a), sin(a)) * lerpf(ARM_INNER, ARM_OUTER, f)) return pts func _draw_mothership_static(canvas: CanvasItem) -> void: var c: Color = MOTHERSHIP_COLOR for i in range(ARM_COUNT): var base: float = (float(i) / float(ARM_COUNT)) * TAU NeonDraw.draw_glowing_polygon(canvas, _talon(base), c, 0.95) # Crystal panel set into the gap behind each arm. var pa: float = base - 0.30 NeonDraw.draw_glowing_polygon(canvas, PackedVector2Array([ Vector2(cos(pa - 0.11), sin(pa - 0.11)) * 54.0, Vector2(cos(pa + 0.11), sin(pa + 0.11)) * 54.0, Vector2(cos(pa + 0.15), sin(pa + 0.15)) * 86.0, Vector2(cos(pa - 0.15), sin(pa - 0.15)) * 86.0, ]), c, 0.7) # Reactor housing: the nested rings around the cell that breathes. NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 38.0, 6, PI / 6.0), c, 1.0) NeonDraw.draw_glowing_polygon(canvas, _ring(Vector2.ZERO, 24.0, 6, 0.0), c, 0.85) const CORE_CELL_RADIUS: float = 12.0 func _build_mothership_pieces() -> void: var core_color: Color = Color(0.75, 1.0, 0.85) # The breathing cell: drawn at a reference size and then scaled, rather # than rebuilt at a new radius every frame. _core_cell = NeonPiece.create(self, Vector2.ZERO, core_color) _core_cell.add_polygon(_ring(Vector2.ZERO, CORE_CELL_RADIUS, 6, PI / 6.0)) # Satellites sit at fixed offsets inside a holder; the orbit is the # holder's rotation, so four hexagon pairs become one transform update. _satellites = Node2D.new() add_child(_satellites) for i in range(SATELLITE_COUNT): var a: float = (float(i) / float(SATELLITE_COUNT)) * TAU var at: Vector2 = Vector2(cos(a), sin(a)) * SATELLITE_ORBIT var shell: NeonPiece = NeonPiece.create(_satellites, at, MOTHERSHIP_COLOR) shell.add_polygon(_ring(Vector2.ZERO, 10.0, 6, PI / 6.0), 0.85) var cell: NeonPiece = NeonPiece.create(_satellites, at, core_color) cell.add_polygon(_ring(Vector2.ZERO, 4.5, 6, PI / 6.0), 0.8) func _animate_mothership() -> void: var breath: float = core_pulse() var radius: float = lerpf(9.0, 15.0, breath) _core_cell.scale = Vector2.ONE * (radius / CORE_CELL_RADIUS) _core_cell.modulate.a = lerpf(0.55, 1.0, breath) _satellites.rotation = orbit_angle() # --- ship crafter: an orbital yard with a hull in the cradle -------------- func _draw_crafter_static(canvas: CanvasItem) -> void: var c: Color = CRAFTER_COLOR # Fabricator core housing. This replaces the chimney - nothing vents # smoke in vacuum. NeonDraw.draw_glowing_polygon(canvas, _ring(CORE_CENTRE, 26.0, 6, PI / 6.0), c, 1.0) # Radiator fins above and below the core, slatted like real ones. for dir_y in [-1.0, 1.0]: var fin: PackedVector2Array = PackedVector2Array([ CORE_CENTRE + Vector2(-16, 30 * dir_y), CORE_CENTRE + Vector2(18, 34 * dir_y), CORE_CENTRE + Vector2(24, 54 * dir_y), CORE_CENTRE + Vector2(-10, 50 * dir_y), ]) NeonDraw.draw_glowing_polygon(canvas, fin, c, 0.75) for f in [0.35, 0.68]: NeonDraw.draw_glowing_line(canvas, fin[0].lerp(fin[3], f), fin[1].lerp(fin[2], f), c, 0.5, 0.28) # Truss spine linking the core to the cradle: two rails with X-bracing, # the standard "this was assembled in orbit" read. var spine_top: float = 4.0 var spine_bottom: float = 22.0 NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_top), Vector2(14, spine_top), c, 0.9, 0.4) NeonDraw.draw_glowing_line(canvas, Vector2(-32, spine_bottom), Vector2(14, spine_bottom), c, 0.9, 0.4) for i in range(3): var x0: float = -32.0 + float(i) * 15.0 var x1: float = x0 + 15.0 NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_top), Vector2(x1, spine_bottom), c, 0.55, 0.28) NeonDraw.draw_glowing_line(canvas, Vector2(x0, spine_bottom), Vector2(x1, spine_top), c, 0.55, 0.28) # Construction cradle: two open ribs holding the hull, with the crane # rail overhead. Open at the top so the finished hull can clear it. NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, 2.10, 4.18, 7), c, 1.0, 0.7) NeonDraw.draw_glowing_path(canvas, _arc(BAY_CENTRE, RIB_RADIUS, -1.04, 1.04, 7), c, 1.0, 0.7) NeonDraw.draw_glowing_line(canvas, Vector2(10, RAIL_Y), Vector2(82, RAIL_Y), c, 0.9, 0.55) for side in [-1.0, 1.0]: NeonDraw.draw_glowing_line(canvas, BAY_CENTRE + Vector2(RIB_RADIUS * side, -6.0), Vector2(BAY_CENTRE.x + 44.0 * side, RAIL_Y), c, 0.7, 0.3) func _build_crafter_pieces() -> void: var c: Color = CRAFTER_COLOR # The core's inner cell: fades with the reactor rather than being redrawn # at a new brightness each frame. _core_cell = NeonPiece.create(self, CORE_CENTRE, Color(1.0, 0.9, 0.6)) _core_cell.add_polygon(_ring(Vector2.ZERO, 12.0, 6, PI / 6.0)) # Docking beacons, blinking out of step with each other. for i in range(2): var lamp: Vector2 = BAY_CENTRE + Vector2(-RIB_RADIUS + 2.0 + float(i) * (RIB_RADIUS * 2.0 - 4.0), 30.0) var beacon: NeonPiece = NeonPiece.create(self, lamp, Color(1.0, 0.95, 0.75)) beacon.add_polygon(_ring(Vector2.ZERO, 3.0, 4)) _beacons.append(beacon) # Clamp arms reaching in from each rib. They let go as the hull launches, # which is a fade rather than a redraw. _clamps = NeonPiece.create(self, Vector2.ZERO, c) for side in [-1.0, 1.0]: var outer: Vector2 = BAY_CENTRE + Vector2(RIB_RADIUS * side, 6.0) var inner: Vector2 = BAY_CENTRE + Vector2(24.0 * side, 6.0) _clamps.add_line(outer, inner, 0.35) _clamps.add_line(inner + Vector2(0, -5), inner + Vector2(0, 5), 0.3) # Crane: the head rides the rail, so only its x moves. The hoist is drawn # one unit long and stretched with scale.y to reach whatever seam is # being welded - stretching a line needs no redraw, redrawing it would. _crane = Node2D.new() add_child(_crane) var head: NeonPiece = NeonPiece.create(_crane, Vector2.ZERO, c) head.add_polygon(PackedVector2Array([ Vector2(-10, -7), Vector2(10, -7), Vector2(6, 6), Vector2(-6, 6), ])) head.add_line(Vector2(0, -7), Vector2(0, RAIL_Y - (RAIL_Y + 13.0)), 0.35, 0.8) _hoist = NeonPiece.create(_crane, Vector2(0, 6), c, 0.5) _hoist.add_line(Vector2.ZERO, Vector2(0, 1), 0.3) # Weld sparks: four spokes on a holder that spins and stretches. The # jitter is genuinely procedural, but rotating and scaling a holder gives # the same flicker without re-tessellating the spokes every frame. _sparks = Node2D.new() add_child(_sparks) for i in range(4): var a: float = float(i) * TAU / 4.0 var spark: NeonPiece = NeonPiece.create(_sparks, Vector2.ZERO, Color(1.0, 0.95, 0.7)) spark.add_line(Vector2.ZERO, Vector2(cos(a), sin(a)) * 11.0, 0.3) # The hull under construction. Each part is its own piece so attaching one # is a `visible` flip, and the launch is the holder's position and alpha. _hull_holder = Node2D.new() add_child(_hull_holder) for i in range(_hull_parts.size()): var part: NeonPiece = NeonPiece.create(_hull_holder, Vector2.ZERO, HULL_COLOR) part.add_polygon(_hull_parts[i]) part.visible = false _hull_pieces.append(part) # Engine wash under the hull, and the completion ring. The ring is drawn # at a reference radius and scaled outwards. _wash = NeonPiece.create(_hull_holder, Vector2.ZERO, Color(1.0, 0.85, 0.45), 0.0) for dx in [-4.0, 4.0]: _wash.add_line(Vector2(dx, 14), Vector2(dx * 1.6, 30.0), 0.45) _flash_ring = NeonPiece.create(self, BAY_CENTRE, HULL_COLOR, 0.0) _flash_ring.add_polygon(_ring(Vector2.ZERO, FLASH_RING_RADIUS, 6, PI / 6.0)) func _animate_crafter() -> void: var t: float = anim_time / CYCLE _core_cell.modulate.a = 0.45 + 0.55 * (0.5 + 0.5 * sin(phase_time * 4.0)) for i in range(_beacons.size()): _beacons[i].modulate.a = 0.15 + 0.45 * (0.5 + 0.5 * sin(phase_time * 6.0 + float(i) * PI)) if t < ASSEMBLE_END: _animate_assembly(t / ASSEMBLE_END) elif t < FLASH_END: _animate_completion_flash((t - ASSEMBLE_END) / (FLASH_END - ASSEMBLE_END)) else: _animate_launch((t - FLASH_END) / (1.0 - FLASH_END)) # How many pieces are on the hull at this point through the assembly phase. # Parts attach in order, so this is just how many thresholds have passed. func attached_part_count(p: float) -> int: var n: int = 0 for part in SHIP_PARTS: if p >= float(part.at): n += 1 return n func _place_hull(centre: Vector2, alpha: float) -> void: # The parts are defined around their own origin, so the holder goes # straight to the point they should be centred on. _hull_holder.position = centre _hull_holder.modulate.a = alpha func _animate_assembly(p: float) -> void: _clamps.visible = true _clamps.modulate.a = 0.8 _flash_ring.modulate.a = 0.0 _wash.modulate.a = 0.0 _place_hull(BAY_CENTRE, 1.0) var next_part: int = attached_part_count(p) for i in range(_hull_pieces.size()): var attached: bool = i < next_part _hull_pieces[i].visible = attached if attached: # Freshly attached parts flare, then settle to full brightness. var age: float = p - float(SHIP_PARTS[i].at) _hull_pieces[i].modulate.a = 1.0 if age > 0.08 else lerpf(0.3, 1.0, age / 0.08) # The crane rides the rail over whichever seam is being worked, and # retracts once there is nothing left to attach. var weld: Vector2 = BAY_CENTRE + (SHIP_PARTS[mini(next_part, SHIP_PARTS.size() - 1)].weld as Vector2) if next_part >= _hull_pieces.size(): weld.y = RAIL_Y + 16.0 _crane.position = Vector2(weld.x, RAIL_Y + 13.0) _hoist.scale.y = maxf(weld.y - (RAIL_Y + 19.0), 1.0) var welding: bool = next_part < _hull_pieces.size() _sparks.visible = welding if welding: var flicker: float = 0.45 + 0.55 * absf(sin(phase_time * 34.0)) _sparks.position = weld _sparks.rotation = phase_time * 7.0 _sparks.scale = Vector2.ONE * (0.55 + 0.45 * flicker) _sparks.modulate.a = flicker func _animate_completion_flash(p: float) -> void: # Whole hull, clamps springing open, and a ring pushing outwards to mark # it as finished. _clamps.modulate.a = 1.0 - p _sparks.visible = false _place_hull(BAY_CENTRE, 1.0) for piece in _hull_pieces: piece.visible = true piece.modulate.a = 1.0 # Retracted, so it does not hang over the finished hull. _crane.position = Vector2(BAY_CENTRE.x, RAIL_Y + 13.0) _hoist.scale.y = 1.0 _flash_ring.modulate.a = 1.0 - p _flash_ring.scale = Vector2.ONE * (lerpf(18.0, 56.0, p) / FLASH_RING_RADIUS) func _animate_launch(p: float) -> void: # Clears the cradle and fades out; the next cycle starts from bare ribs. _clamps.modulate.a = 0.0 _sparks.visible = false _flash_ring.modulate.a = 0.0 var eased: float = p * p var fade: float = clampf(1.0 - p * 1.15, 0.0, 1.0) _place_hull(BAY_CENTRE + Vector2(0.0, -124.0 * eased), fade) for piece in _hull_pieces: piece.visible = true piece.modulate.a = 1.0 # Engine wash, strongest just after it breaks free of the clamps. _wash.modulate.a = 0.4 + 0.6 * sin(minf(p * 3.2, PI))