extends Node2D # By path, not by its `ResourcePickupVFX` global name: a bare class-name # reference needs the global class cache already warm, which it is not when # this script is reached indirectly - loading Main.tscn from inside a preview # scene made it a hard parse error. Same workaround PlanetSetups and # PlanetSetupEditor already use. const RESOURCE_PICKUP_VFX = preload("res://vfx/ResourcePickupVFX.gd") @export var fleet_path: NodePath = NodePath("../Fleet") @onready var fleet: Node2D = get_node(fleet_path) const SOURCE_COLOUR: Color = Color(0.85, 0.52, 1.0) const CORE_COLOUR: Color = Color(0.15, 0.03, 0.22) # Offsets from a vent's centre, and each bubble's radius. const SOURCE_BUBBLES: Array = [ [Vector2(-20.0, 8.0), 20.0], [Vector2(18.0, 14.0), 27.0], [Vector2(17.0, -22.0), 15.0], ] const BUBBLES_PER_VENT: int = 4 const BUBBLE_MIN_RADIUS: float = 5.0 const BUBBLE_MAX_RADIUS: float = 14.0 const EMISSION_COLOUR: Color = Color(0.92, 0.68, 1.0) var vents: Array[Vector2] = [] # One row of rising-bubble pieces per vent. var bubbles: Array = [] var progress: float = 0.0 var emission_phase: float = 0.0 func _ready() -> void: # #91: the vents are ground features and must never cover the fleet. # SurfaceFeatures is added AFTER Fleet in PlanetSurface.tscn, so at the # default z_index of 0 the later sibling simply wins and the player flies # UNDER their own gas source. Same fix, and the same reason, as #50's # capture rings and the rocks on a rocky surface (Rock.gd) - the # difference is that this field draws in its own _draw() rather than # spawning child nodes, so the depth belongs on the field itself. z_index = -1 var rng := RandomNumberGenerator.new() rng.seed = hash(GameState.planet_id if GameState.planet_id != "" else GameState.planet_name) for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650))) _build_sources() _build_bubbles() _place_bubbles() # The sources never move, never change colour and never fade - they were # simply being re-tessellated every frame alongside the bubbles rising out of # them, because one _draw() covered both. Eighteen glowing polygons a frame, # each of which NeonDraw expands into four antialiased polylines. Rasterised # once into a piece each now, per the rule issues #6 and #10 set. func _build_sources() -> void: for vent in vents: var piece: NeonPiece = NeonPiece.create(self, vent, SOURCE_COLOUR) piece.core_color = CORE_COLOUR piece.z_index = -1 for bubble in SOURCE_BUBBLES: var ring := PackedVector2Array() for i in range(12): var angle: float = TAU * float(i) / 12.0 ring.append(Vector2(bubble[0]) + Vector2(cos(angle), sin(angle)) * float(bubble[1])) piece.add_polygon(ring, 0.9, 0.6) # One piece per rising bubble, built as a UNIT circle and thereafter only # moved, scaled and faded. The bubble genuinely changes size and opacity as # it climbs, which is why this used to redraw - but size is scale and opacity # is modulate, and neither needs the geometry rebuilding. func _build_bubbles() -> void: for vent in vents: var row: Array = [] for i in range(BUBBLES_PER_VENT): var piece: NeonPiece = NeonPiece.create(self, vent, EMISSION_COLOUR) piece.core_color = CORE_COLOUR piece.z_index = -1 # Built at the LARGEST size a bubble reaches and scaled DOWN, # never up. Scaling a piece scales its stroke with it, and a unit # circle blown up to radius 14 took NeonDraw's widths with it - # a hundred pixels of glow around a fourteen pixel bubble, which # rendered as a bright blob rather than a bubble. Downward, the # stroke only ever thins, which is what a smaller bubble wants. var ring := PackedVector2Array() for point in range(10): var angle: float = TAU * float(point) / 10.0 ring.append(Vector2(cos(angle), sin(angle)) * BUBBLE_MAX_RADIUS) piece.add_polygon(ring, 0.75, 0.45) row.append(piece) bubbles.append(row) # Where one bubble sits, how big it is and how faded, for a point in its # climb. Unchanged arithmetic - only the thing it is applied to changed. func _place_bubbles() -> void: for v in range(vents.size()): for i in range(BUBBLES_PER_VENT): var travel: float = fmod(emission_phase + float(i) * 0.25, 1.0) var piece: NeonPiece = bubbles[v][i] piece.position = vents[v] + Vector2( sin((travel + float(i)) * TAU) * 11.0, -48.0 - travel * 92.0) var radius: float = lerpf(BUBBLE_MIN_RADIUS, BUBBLE_MAX_RADIUS, travel) var k: float = radius / BUBBLE_MAX_RADIUS piece.scale = Vector2(k, k) piece.modulate.a = 1.0 - travel * 0.72 func _process(delta: float) -> void: emission_phase = fmod(emission_phase + delta * 0.55, 1.0) # Node properties only. Nothing is rasterised after the first frame. _place_bubbles() for vent in vents: if fleet.get_centroid().distance_to(vent) < 90.0: progress += delta if progress >= 1.0: # #56: a full fleet hold rejects the deposit - keep the # undelivered whole units in progress rather than discarding # them, so gas is only ever lost by not gathering it. var whole: int = int(progress) var taken: int = GameState.collect_gas_cargo(whole) progress -= float(taken) RESOURCE_PICKUP_VFX.spawn(get_tree().root, to_global(vent), GameState.RESOURCE_GAS, taken) return progress = 0.0