2026-08-15 21:22:10 +02:00
|
|
|
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
|
2026-09-02 13:43:56 +02:00
|
|
|
# #177: holding an already-cleared point should resolve briskly. Eight seconds
|
|
|
|
|
# made every point feel like waiting after the fight was over.
|
|
|
|
|
@export var capture_time: float = 4.0
|
2026-08-15 21:22:10 +02:00
|
|
|
@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:
|
2026-08-15 22:47:44 +02:00
|
|
|
# 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
|
2026-08-15 21:22:10 +02:00
|
|
|
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)
|