2026-08-14 21:44:28 +02:00
|
|
|
class_name BuildingVisualBase
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
const GAS_BURNER_COLOR: Color = Color(0.95, 0.35, 0.15)
|
|
|
|
|
const GAS_BURNER_ENERGY_COLOR: Color = Color(0.85, 0.95, 0.15)
|
2026-08-14 21:44:28 +02:00
|
|
|
|
|
|
|
|
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
|
|
|
|
# over it. Reads as a workshop rather than a factory - the crafter already
|
|
|
|
|
# owns "assembly line", so this is measurement, not production.
|
|
|
|
|
const LAB_FRAME_W: float = 78.0
|
|
|
|
|
const LAB_FRAME_H: float = 58.0
|
|
|
|
|
const LAB_MOUNT_RADIUS: float = 15.0
|
|
|
|
|
const LAB_SCANNER_ORBIT: float = 96.0
|
|
|
|
|
const LAB_SCAN_SPEED: float = 0.9
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
# Sweeping instrument booms at a ship lab (#5).
|
|
|
|
|
var _lab_scanners: Node2D = null
|
|
|
|
|
var _flash_ring: NeonPiece = null
|
|
|
|
|
var _wash: NeonPiece = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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 register_draw() -> void:
|
|
|
|
|
draw_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- ship lab -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func _draw_ship_lab_static(target: CanvasItem) -> void:
|
|
|
|
|
var c: Color = SHIP_LAB_COLOR
|
|
|
|
|
|
|
|
|
|
# Outer test frame, open at the corners so it reads as a rig rather than
|
|
|
|
|
# a solid hull.
|
|
|
|
|
for sx in [-1.0, 1.0]:
|
|
|
|
|
for sy in [-1.0, 1.0]:
|
|
|
|
|
var corner: Vector2 = Vector2(sx * LAB_FRAME_W, sy * LAB_FRAME_H)
|
|
|
|
|
NeonDraw.draw_glowing_line(target, corner,
|
|
|
|
|
corner - Vector2(sx * LAB_FRAME_W * 0.45, 0.0), c, 0.9, 0.42)
|
|
|
|
|
NeonDraw.draw_glowing_line(target, corner,
|
|
|
|
|
corner - Vector2(0.0, sy * LAB_FRAME_H * 0.5), c, 0.9, 0.42)
|
|
|
|
|
|
|
|
|
|
# The vessel under test, held nose-up in the middle - deliberately the
|
|
|
|
|
# fleet's own silhouette, so it is obvious what the lab is for.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, PackedVector2Array([
|
|
|
|
|
Vector2(0, -26), Vector2(13, 12), Vector2(0, 5), Vector2(-13, 12),
|
|
|
|
|
]), HULL_COLOR, 0.95)
|
|
|
|
|
|
|
|
|
|
# Mount ring and its bracing.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, LAB_MOUNT_RADIUS, 6, PI / 6.0), c, 0.5,
|
|
|
|
|
NeonDraw.DEFAULT_CORE, 0.7)
|
|
|
|
|
for i in range(4):
|
|
|
|
|
var a: float = (float(i) / 4.0) * TAU + PI / 4.0
|
|
|
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
|
|
|
NeonDraw.draw_glowing_line(target, dir * LAB_MOUNT_RADIUS, dir * 40.0, c, 0.45, 0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _build_lab_pieces() -> void:
|
|
|
|
|
# Two instrument booms sweeping round the vessel. One holder, rasterised
|
|
|
|
|
# once, rotated per frame (#10).
|
|
|
|
|
_lab_scanners = Node2D.new()
|
|
|
|
|
add_child(_lab_scanners)
|
|
|
|
|
for i in range(2):
|
|
|
|
|
var a: float = float(i) * PI
|
|
|
|
|
var at: Vector2 = Vector2(cos(a), sin(a)) * LAB_SCANNER_ORBIT
|
|
|
|
|
var piece: NeonPiece = NeonPiece.create(_lab_scanners, at, SHIP_LAB_COLOR)
|
|
|
|
|
piece.add_polygon(PackedVector2Array([
|
|
|
|
|
Vector2(-4, -12), Vector2(4, -12), Vector2(4, 12), Vector2(-4, 12),
|
|
|
|
|
]))
|
|
|
|
|
piece.add_line(Vector2(0, 0), Vector2(-cos(a), -sin(a)) * 26.0, 0.3, 0.5)
|
|
|
|
|
piece.rotation = a
|
|
|
|
|
piece.queue_redraw()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _animate_ship_lab() -> void:
|
|
|
|
|
_lab_scanners.rotation = phase_time * LAB_SCAN_SPEED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 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
|
|
|
|
|
|
|
|
|
|
|
Gas burner building and the energy resource (#54)
A new building that consumes gas to produce energy, a new resource
stored and moved exactly like ore and gas.
The resource: EnergyResourceGlyph (a cell/bolt mark) + energy's own
warm yellow-green ResourceDefinition, registered in
ResourceDefinitions.ORDER/DEFINITIONS and GameState.RESOURCE_ENERGY.
Inventory/ResourceBar/ResourceTransferList needed no changes - #49
already made them resource-agnostic, confirmed structurally in the
test rather than touched.
The building: GasBurnerBuildingVisual (a hexagonal chamber with intake
vents in gas's colour and output ports in energy's, so the silhouette
itself reads "gas in, energy out"), GasBurnerBuildingDefinition
(capabilities=["gas_burner"], its own build-menu icon appended to
IconButton.Icon rather than inserted, per the ticket's own warning
about renumbering stored scene values), and the matching
UI/BuildMenu/GasBurner* nodes in Main.tscn. Gated behind a new
"Power Systems" skill-tree node, mirroring how "freight" unlocks the
transport hub.
Conversion: _advance_gas_burners(), simulated in GameState alongside
_advance_production/extractor mining so it keeps running while the
player is elsewhere. Withdraws gas and deposits energy in the
building's OWN inventory only - never the bank - so it stalls with no
local gas exactly like a lab waiting on materials.
Logistics: a gas burner's ongoing gas deficit is folded into the
existing queued-building-supply dispatch path (same "needs resource X"
loop production/research orders already use, extended with
gas_burner_gas_deficit()), and a new TO_BUILDING_PICKUP/
TO_MOTHERSHIP_FROM_BUILDING transporter state pair collects its energy
to the bank, mirroring the extractor pickup path per the ticket. The
existing f5c60f8 priority (queued deliveries outrank collection) holds:
the pickup dispatch loop runs last.
Tests: new tests/test_gas_burner.gd (resource genericity, stall
semantics, research gate, and full delivery+pickup logistics via a
real hub), run_tests transporter/resource regressions, and screenshots
of the build menu (all 7 buildings) and transfer menu (all 3
resources). Full suite: 81/81.
Two real bugs found and fixed along the way, neither gas-burner-
specific:
- _transporter_inbound_to() matched ANY non-idle transporter with a
matching target_id, regardless of purpose. Once pickup states shared
target_id with delivery states (both address a building, not an
extractor), a transporter delivering gas made the SAME building look
"busy" to the unrelated energy-pickup dispatch, starving it even
with a second, genuinely free transporter sitting idle at the hub.
Split into _transporter_inbound_to() (delivery states only) and a
new _transporter_inbound_for_pickup() (pickup states only).
- The build menu's 3-row/6-slot layout (MENU_ROW_TOPS + row content
sizing) had no room for a 7th building - despite the ticket's own
note that no layout changes should be needed. A naive 4th row ran
off the bottom of a 720x1280 viewport; fitting one meant both adding
a 4th row top AND shrinking the shared button/label/cost sizing so
each row's content no longer taller than the compressed row spacing.
Caught only by actually screenshotting it - a real build menu, not
headless, since --headless renders nothing.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 12:10:31 +02:00
|
|
|
# --- gas burner (#54) ------------------------------------------------------
|
|
|
|
|
# A squat furnace rather than more scaffolding: a hexagonal combustion
|
|
|
|
|
# chamber with a hot inner core, fed by intake vents drawn in gas's own
|
|
|
|
|
# colour and exhausting through ports glowing in energy's colour - so the
|
|
|
|
|
# structure itself reads as "gas in, energy out" without needing a label.
|
|
|
|
|
# Drawn once and never redrawn, same as the command centre.
|
|
|
|
|
|
|
|
|
|
const BURNER_CHAMBER_RADIUS: float = 50.0
|
|
|
|
|
const BURNER_CORE_RADIUS: float = 34.0
|
|
|
|
|
const BURNER_VENT_COUNT: int = 4
|
|
|
|
|
const BURNER_VENT_LENGTH: float = 26.0
|
|
|
|
|
const BURNER_PORT_RADIUS: float = 6.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _draw_gas_burner_static(target: CanvasItem) -> void:
|
|
|
|
|
var c: Color = GAS_BURNER_COLOR
|
|
|
|
|
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CHAMBER_RADIUS, 6, PI / 6.0), c, 1.0)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CORE_RADIUS, 6, PI / 6.0), c, 0.5,
|
|
|
|
|
NeonDraw.DEFAULT_CORE, 0.7)
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, _ring(Vector2.ZERO, BURNER_CORE_RADIUS * 0.5, 6, PI / 6.0),
|
|
|
|
|
Color(1.0, 0.75, 0.35), 0.85)
|
|
|
|
|
|
|
|
|
|
for i in range(BURNER_VENT_COUNT):
|
|
|
|
|
var a: float = (float(i) / BURNER_VENT_COUNT) * TAU + PI / 4.0
|
|
|
|
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
|
|
|
|
var tip: Vector2 = dir * (BURNER_CHAMBER_RADIUS + BURNER_VENT_LENGTH)
|
|
|
|
|
# Intake vent in gas's own colour, so it is unmistakably the feed
|
|
|
|
|
# rather than another structural brace.
|
|
|
|
|
NeonDraw.draw_glowing_line(target, dir * BURNER_CHAMBER_RADIUS, tip,
|
|
|
|
|
Color(0.85, 0.52, 1.0), 0.7, 0.34)
|
|
|
|
|
# Output port glowing in energy's colour, at the same vent tip -
|
|
|
|
|
# the building's own silhouette shows both ends of the conversion.
|
|
|
|
|
NeonDraw.draw_glowing_polygon(target, _ring(tip, BURNER_PORT_RADIUS, 5), GAS_BURNER_ENERGY_COLOR, 0.9)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 21:44:28 +02:00
|
|
|
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))
|