2026-09-04 21:46:24 +02:00
|
|
|
class_name ScannerGuidance
|
|
|
|
|
extends Control
|
|
|
|
|
|
|
|
|
|
const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
|
|
|
|
|
const EDGE_MARGIN: float = 34.0
|
|
|
|
|
|
2026-09-05 12:16:17 +02:00
|
|
|
# The scene sets process_mode = ALWAYS. A narration line stops the tree
|
|
|
|
|
# (NarrationBanner, #128) and the prologue is nearly all narration, so a
|
|
|
|
|
# pausable marker would be absent for most of the run it exists to guide
|
|
|
|
|
# (#207). Nothing here simulates anything - it reads a target and points at
|
|
|
|
|
# it - so running while the game is stopped costs nothing and stops the
|
|
|
|
|
# marker blinking out every time somebody speaks.
|
2026-09-04 21:46:24 +02:00
|
|
|
@onready var indicator: Control = $Indicator
|
|
|
|
|
@onready var target_label: Label = $TargetLabel
|
|
|
|
|
|
|
|
|
|
var fleet: Node2D
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
fleet = get_tree().get_first_node_in_group("player_fleet") as Node2D
|
|
|
|
|
visible = false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
2026-09-05 12:16:17 +02:00
|
|
|
# #207: the prologue's destination comes through the same marker, so this
|
|
|
|
|
# no longer hides for the intro - GameState decides which target is live,
|
|
|
|
|
# and an empty answer is what hides it.
|
|
|
|
|
var target: Dictionary = GameState.active_guidance_target()
|
|
|
|
|
if target.is_empty() or _modal_is_open():
|
2026-09-04 21:46:24 +02:00
|
|
|
visible = false
|
|
|
|
|
return
|
|
|
|
|
if fleet == null or not is_instance_valid(fleet):
|
|
|
|
|
fleet = get_tree().get_first_node_in_group("player_fleet") as Node2D
|
|
|
|
|
if fleet == null:
|
|
|
|
|
visible = false
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
visible = true
|
|
|
|
|
var target_world := Vector2(target.world_position)
|
|
|
|
|
var viewport_size: Vector2 = get_viewport_rect().size
|
|
|
|
|
var screen_position: Vector2 = get_viewport().get_canvas_transform() * target_world
|
|
|
|
|
var safe_margin: Vector2 = Vector2.ONE * EDGE_MARGIN + indicator.size * 0.5
|
|
|
|
|
var safe_rect := Rect2(safe_margin, viewport_size - safe_margin * 2.0)
|
|
|
|
|
var at: Vector2 = screen_position if safe_rect.has_point(screen_position) \
|
|
|
|
|
else edge_position(viewport_size, screen_position - viewport_size * 0.5)
|
|
|
|
|
indicator.position = at - indicator.size * 0.5
|
|
|
|
|
var toward_target: Vector2 = target_world - fleet.get_centroid()
|
|
|
|
|
if toward_target.length_squared() > 0.01:
|
|
|
|
|
indicator.rotation = toward_target.angle() + PI / 2.0
|
|
|
|
|
|
2026-09-05 12:16:17 +02:00
|
|
|
# A scanned planet colours the arrow with its own map colour. The World
|
|
|
|
|
# Ship is not a planet and has no definition, so it keeps the indicator's
|
|
|
|
|
# authored colour rather than being forced into one.
|
2026-09-04 21:46:24 +02:00
|
|
|
var definition: Resource = PLANET_DEFINITIONS.get_definition(
|
2026-09-05 12:16:17 +02:00
|
|
|
String(target.get("definition_id", "")))
|
2026-09-04 21:46:24 +02:00
|
|
|
if definition != null:
|
|
|
|
|
indicator.glow_color = definition.map_color
|
|
|
|
|
indicator.queue_redraw()
|
|
|
|
|
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
|
|
|
|
|
target_label.text = "%s · %.1f SECTORS" % [
|
|
|
|
|
String(target.get("name", "SCANNER TARGET")).to_upper(), sectors]
|
|
|
|
|
_place_label(at, viewport_size)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func edge_position(viewport_size: Vector2, direction: Vector2) -> Vector2:
|
|
|
|
|
var center: Vector2 = viewport_size * 0.5
|
|
|
|
|
if direction.length_squared() <= 0.01:
|
|
|
|
|
return center
|
|
|
|
|
var half: Vector2 = center - Vector2.ONE * EDGE_MARGIN - indicator.size * 0.5
|
|
|
|
|
var scale_to_edge: float = minf(half.x / maxf(absf(direction.x), 0.001),
|
|
|
|
|
half.y / maxf(absf(direction.y), 0.001))
|
|
|
|
|
return center + direction * scale_to_edge
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _place_label(indicator_center: Vector2, viewport_size: Vector2) -> void:
|
|
|
|
|
var toward_center: Vector2 = viewport_size * 0.5 - indicator_center
|
|
|
|
|
if toward_center.length_squared() < 0.01:
|
|
|
|
|
toward_center = Vector2.DOWN
|
|
|
|
|
var wanted: Vector2 = indicator_center + toward_center.normalized() * 105.0 \
|
|
|
|
|
- target_label.size * 0.5
|
|
|
|
|
target_label.position = Vector2(
|
|
|
|
|
clampf(wanted.x, EDGE_MARGIN, viewport_size.x - target_label.size.x - EDGE_MARGIN),
|
|
|
|
|
clampf(wanted.y, EDGE_MARGIN, viewport_size.y - target_label.size.y - EDGE_MARGIN))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _modal_is_open() -> bool:
|
|
|
|
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
|
|
|
|
if menu.is_visible_in_tree():
|
|
|
|
|
return true
|
|
|
|
|
return false
|