extends Node2D # Same informal interface as GasGiantVisual/RockyPlanetVisual - configure(), # extractor_anchor(), animates via its own _process(). Deliberately angular # and armoured rather than a natural silhouette (#63: this is an ENEMY # installation, not a celestial body), with a rotating dish and blinking # warning lights to read as occupied/hostile at a glance. # # Built out of NeonPiece children rather than redrawn each frame. Issues #6 # and #10 established that rule for the buildings: every NeonDraw call # expands into four antialiased polylines, so re-running _draw() to advance # an animation re-tessellates the whole thing. This visual was doing exactly # that - nine draw calls a frame, of which eight were fixed geometry - to # rotate one dish and fade six lights. Both are node properties: the dish is # a child that spins, the lights are a child whose modulate breathes, and the # hull is rasterised once and never touched again. var phase: float = 0.0 const RADIUS: float = 108.0 const HULL_COLOR: Color = Color(0.55, 0.1, 0.08) const CORE_COLOR: Color = Color(0.9, 0.25, 0.18) const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15) const DARK_CORE: Color = Color(0.14, 0.02, 0.02) var _dish: NeonPiece var _lights: NeonPiece func _ready() -> void: _build() func configure(_shape_seed: int) -> void: if _dish == null: _build() func extractor_anchor() -> Vector2: return Vector2(0.0, -(RADIUS + 40.0)) func _build() -> void: for child in get_children(): child.queue_free() # The armoured hull and its core: fixed, so drawn once. var shell: NeonPiece = NeonPiece.create(self, Vector2.ZERO, HULL_COLOR) shell.core_color = DARK_CORE shell.add_polygon(_ring(RADIUS, 0.0), 0.85) var core: NeonPiece = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR) core.core_color = DARK_CORE core.add_polygon(_ring(RADIUS * 0.55, PI / 6.0), 0.9) # The dish, drawn along +X and then SPUN by rotating this node - the # rotation is what used to cost a full re-tessellation every frame. _dish = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR) _dish.core_color = DARK_CORE var tip: Vector2 = Vector2(RADIUS * 1.35, 0.0) _dish.add_line(Vector2.ZERO, tip, 0.55, 0.8) _dish.add_polygon(_circle(tip, 11.0), 1.0) # The warning lights blink by fading the whole piece, not by redrawing # six circles at a new alpha. _lights = NeonPiece.create(self, Vector2.ZERO, WARNING_COLOR) _lights.core_color = DARK_CORE for i in range(6): var angle: float = TAU * float(i) / 6.0 _lights.add_polygon(_circle(Vector2(cos(angle), sin(angle)) * RADIUS, 5.0), 1.0) func _ring(radius: float, offset: float) -> PackedVector2Array: var points: PackedVector2Array = PackedVector2Array() for i in range(6): var angle: float = TAU * float(i) / 6.0 + offset points.append(Vector2(cos(angle), sin(angle)) * radius) return points func _circle(centre: Vector2, radius: float) -> PackedVector2Array: var points: PackedVector2Array = PackedVector2Array() for i in range(12): var angle: float = TAU * float(i) / 12.0 points.append(centre + Vector2(cos(angle), sin(angle)) * radius) return points func _process(delta: float) -> void: phase += delta # Two node properties. Nothing is rasterised again. if _dish != null: _dish.rotation = phase * 0.6 if _lights != null: _lights.modulate.a = 0.4 + 0.6 * absf(sin(phase * 4.0))