Forgejo #16, part 3 of #12. BuildController deducted the cost from the bank at the moment of purchase and refused outright with "Not enough rocks", so nothing ever had to be delivered anywhere. Each building now carries its own stock, and an order it cannot pay for yet is QUEUED and completes by itself once ore arrives. Records get an `id` because a building has to be referable across the space <-> planet-surface scene swap, when every Building NODE has been destroyed and rebuilt. Position would nearly work and then fail on two buildings placed on the same spot. Production advances in the GameState autoload, the same way extractor haulers already do, so an order can complete while the player is down a gravity well. A finished vessel becomes `ships_owed` rather than being handed straight to a Fleet that may not exist, and the fleet claims what it is owed when it next has room. The craft menu remembers which crafter it was opened for: _process() clears current_building while any menu is open, so by the next frame the menu would no longer know. It shows the ore delivered HERE rather than the bank, since showing the bank would promise something the crafter cannot actually build. Ship crafting cannot be supplied until #17 lands - nothing yet moves ore into a building. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
531 lines
21 KiB
GDScript
531 lines
21 KiB
GDScript
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)
|
|
# 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 = []
|
|
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()
|
|
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()
|
|
|
|
|
|
# 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)
|
|
_:
|
|
_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
|
|
|
|
|
|
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))
|