class_name CapturePoint extends Area2D # One point on a landed planet's surface that must be held (fleet ship # nearby, no enemy nearby) for capture_time seconds. Progress is polled by # distance each frame rather than through physics signals - same idiom as # RockField/GasVentField - since no collision response is needed, only # proximity. signal captured enum State { NEUTRAL, BLOCKED, CAPTURING, CAPTURED } @export var point_index: int = 0 @export var capture_radius: float = 220.0 @export var capture_time: float = 8.0 @export var enemy_clear_radius: float = 700.0 @export var drain_rate: float = 0.5 const NEUTRAL_COLOR: Color = Color(0.55, 0.85, 1.0) const BLOCKED_COLOR: Color = Color(1.0, 0.3, 0.3) const CAPTURING_COLOR: Color = Color(0.4, 1.0, 0.55) const CAPTURED_COLOR: Color = Color(1.0, 0.85, 0.3) const CORE_RADIUS: float = 60.0 var progress: float = 0.0 var state: int = State.NEUTRAL func _ready() -> void: # Behind the fleet and enemies, same convention as every other world # object that must never occlude them (Planet.gd, Rock.gd, Building.gd, # Hauler.gd, Transporter.gd, VesselDelivery.gd all set this in _ready()). # "Is an enemy inside my capture radius" is the core read of this # mechanic, so a ship or enemy sitting in the ring must stay fully visible. z_index = -1 queue_redraw() # Called by PlanetSetupHost when this point was already captured on a # previous visit, so re-landing restores it instead of starting the fill # over. Permanent for the run - _process() below returns early once # CAPTURED and never re-evaluates it. func mark_captured() -> void: progress = 1.0 state = State.CAPTURED queue_redraw() func _process(delta: float) -> void: if state == State.CAPTURED: return var enemy_nearby: bool = _enemy_nearby() var ship_in_range: bool = _ship_in_range() if enemy_nearby: state = State.BLOCKED progress = maxf(progress - drain_rate * delta / capture_time, 0.0) elif ship_in_range: state = State.CAPTURING progress = minf(progress + delta / capture_time, 1.0) if progress >= 1.0: state = State.CAPTURED GameState.mark_point_captured(GameState.planet_id, point_index) captured.emit() else: state = State.NEUTRAL progress = maxf(progress - drain_rate * delta / capture_time, 0.0) queue_redraw() func _ship_in_range() -> bool: for ship in get_tree().get_nodes_in_group("player_ships"): if is_instance_valid(ship) and global_position.distance_to(ship.global_position) <= capture_radius: return true return false func _enemy_nearby() -> bool: for enemy in get_tree().get_nodes_in_group("enemies"): if is_instance_valid(enemy) and global_position.distance_to(enemy.global_position) <= enemy_clear_radius: return true return false func _state_color() -> Color: match state: State.BLOCKED: return BLOCKED_COLOR State.CAPTURING: return CAPTURING_COLOR State.CAPTURED: return CAPTURED_COLOR _: return NEUTRAL_COLOR func _draw() -> void: var color: Color = _state_color() # Outer ring: the actual capture radius, faint - tells the player how # close they need to stay without dominating the screen. _draw_ring(capture_radius, 0.0, TAU, color, 0.35) # Inner beacon: fixed size regardless of capture_radius, so it reads # clearly even when zoomed out. A filling arc sweeps clockwise from the # top as progress advances, same "fill toward completion" idea as # VesselGasBomberComponent's hull fill, adapted to a ring since a # capture point has no hull. _draw_ring(CORE_RADIUS, 0.0, TAU, color, 1.0) if progress > 0.001: var sweep: float = TAU * progress _draw_ring(CORE_RADIUS, -PI / 2.0, -PI / 2.0 + sweep, color, 1.0, 10.0) func _draw_ring(radius: float, from_angle: float, to_angle: float, color: Color, alpha: float, core_width: float = 6.0) -> void: draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(color, 0.16 * alpha), core_width * 2.6, true) draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(color, 0.5 * alpha), core_width, true) draw_arc(Vector2.ZERO, radius, from_angle, to_angle, 64, Color(1.0, 0.96, 0.9, 0.9 * alpha), core_width * 0.3, true)