extends Area2D # A gatherable crystal of asteroid ore lying on a planet's surface. Picked # up by flying any ship over it; the fleet doesn't need to stop. # # Deliberately drawn as a PERFECT HEXAGON rather than the irregular lumpy # polygon Asteroid.gd uses. The two now DO appear together - shooting an # asteroid drops one of these right next to its neighbours - which makes the # shape language matter more than when they were kept apart by scene: jagged # and irregular reads as a natural hazard, regular and faceted reads as # something valuable and meant to be picked up. # # It also drifts toward a nearby ship (see _physics_process). Nothing # harmful moves helpfully toward you, so the pull doubles as a readability # cue on top of making gathering feel less fiddly. signal collected # 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://ResourcePickupVFX.gd") # Seconds before an uncollected rock disappears. 0 means never, which is what # hand-placed surface rocks use - a planet surface is bounded and authored, so # nothing accumulates. Ore dropped by a shot asteroid sets a real lifetime, # because open space is unbounded and a field shot up and left behind would # otherwise leak nodes for the rest of the session. @export var lifetime: float = 0.0 @export var attract_radius: float = 220.0 @export var attract_speed: float = 260.0 @export var attract_accel: float = 420.0 var shape_points: PackedVector2Array = PackedVector2Array() var facet_points: PackedVector2Array = PackedVector2Array() var size_scale: float = 1.0 var velocity: Vector2 = Vector2.ZERO var age: float = 0.0 func _ready() -> void: add_to_group("rocks") z_index = -1 area_entered.connect(_on_area_entered) queue_redraw() func build_shape(shape_seed: int) -> void: # Only the SIZE varies, never the silhouette: every ore node is the same # regular hexagon, which is the whole point of the cue. var rng: RandomNumberGenerator = RandomNumberGenerator.new() rng.seed = shape_seed size_scale = rng.randf_range(0.85, 1.25) var r: float = 13.0 * size_scale shape_points = PackedVector2Array() for i in range(6): var a: float = (float(i) / 6.0) * TAU - PI / 2.0 shape_points.append(Vector2(cos(a), sin(a)) * r) # One flat-to-flat line across the middle, so it reads as a cut face # rather than a plain outline at small sizes. facet_points = PackedVector2Array([ shape_points[5], shape_points[2], ]) queue_redraw() func _physics_process(delta: float) -> void: # Drift toward the closest ship inside attract_radius. No redraw here: # position is a transform property and the crystal's shape is fixed. if lifetime > 0.0: age += delta if age >= lifetime: queue_free() return var target: Node2D = _nearest_ship() if target == null: velocity = velocity.move_toward(Vector2.ZERO, attract_accel * delta) else: var desired: Vector2 = (target.position - position).normalized() * attract_speed velocity = velocity.move_toward(desired, attract_accel * delta) position += velocity * delta func _nearest_ship() -> Node2D: var nearest: Node2D = null var best: float = attract_radius for ship in get_tree().get_nodes_in_group("player_ships"): if not is_instance_valid(ship): continue var d: float = position.distance_to(ship.position) if d <= best: best = d nearest = ship return nearest func _on_area_entered(area: Area2D) -> void: if area.is_in_group("player_ships"): # Into the fleet's hold, not the bank (#14). It only counts as # spendable once the fleet has flown it back to the mothership. # #56: the hold has a finite total_capacity now - a full fleet must # leave the rock behind rather than despawning it for nothing. var taken: int = GameState.collect_cargo(1) if taken <= 0: return # #79: say what was picked up, where it was picked up. Parented to the # root rather than to this node, which is about to be freed. RESOURCE_PICKUP_VFX.spawn(get_tree().root, global_position, GameState.RESOURCE_ORE, taken) collected.emit() queue_free() func _draw() -> void: if shape_points.is_empty(): return # Deep teal with a near-black core: a dark, saturated colour reads # clearly against the bright lunar regolith (sampled at ~0.75 grey), # where the original warm yellow disappeared into it. var glow: Color = Color(0.0, 0.62, 0.7) var core: Color = Color(0.0, 0.12, 0.15) NeonDraw.draw_glowing_polygon(self, shape_points, glow, 1.0, core) # One line only - the hexagon has a single flat-to-flat facet. This used # to draw two lines from a 4-point array; the hexagon rewrite left only # 2 points, so the second line indexed out of bounds and every rock # threw on every draw. if facet_points.size() >= 2: NeonDraw.draw_glowing_line(self, facet_points[0], facet_points[1], glow, 0.7, 0.3, core)