spaceship/ui/ScannerGuidance.gd

98 lines
4.2 KiB
GDScript3
Raw Permalink Normal View History

class_name ScannerGuidance
extends Control
const PLANET_DEFINITIONS = preload("res://planets/PlanetDefinitions.gd")
const EDGE_MARGIN: float = 34.0
# 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.
@onready var indicator: Control = $Indicator
@onready var target_label: Label = $TargetLabel
var fleet: Node2D
Stop redrawing what only needs moving Reviewed against the rule issues #6 and #10 set for the buildings: geometry is rasterised once and thereafter only moved, scaled or faded, because every NeonDraw call expands into four antialiased polylines. The planet visuals never got that refactor. The black hole rebuilt ten glowing paths of twenty-five points every frame - forty polylines - to spin five rings; each ring is a NeonPiece that rotates now. The research lab spent nine draw calls a frame, eight of them fixed geometry, to turn one dish and blink six lights; the dish is a child that rotates and the lights a child whose modulate breathes. The gas giant's bands genuinely deform, so they still draw, but its constant rim moved out to a child. NeonPiece gained add_path, which it needed for open arcs. And I had broken the rule myself. ScannerGuidance re-queued a redraw of its arrow every frame for a colour that changes only with the destination - and it runs with PROCESS_MODE_ALWAYS, so it was re-tessellating through every dialog that stops the rest of the game. Guarded, and pinned by a test that counts CanvasItem's own draw signal: 36 rasterisations in 0.6s before, one after. Two smaller things from the same pass: the station menu drew its condition bar by hand where the ship lab already uses a ProgressBar node, so it uses one now and its _draw is gone; and station/upkeep refreshes were wired into _refresh_garrison(), which nothing but a test calls, so both menus showed whatever was true when they opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-08 11:35:45 +02:00
# The colour the arrow was last rasterised with. Issues #6/#10: a glyph is
# drawn once and thereafter moved and rotated, both of which reuse the cached
# geometry. Re-queueing a redraw every frame for a colour that changes only
# when the DESTINATION changes threw that away - and this node runs even
# while the game is stopped for a dialog, so it was the one piece of UI
# re-tessellating itself through the quietest moments in the game.
var _drawn_glow: Color = Color(0, 0, 0, 0)
func _ready() -> void:
fleet = get_tree().get_first_node_in_group("player_fleet") as Node2D
visible = false
func _process(_delta: float) -> void:
# #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():
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
# 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.
var definition: Resource = PLANET_DEFINITIONS.get_definition(
String(target.get("definition_id", "")))
Stop redrawing what only needs moving Reviewed against the rule issues #6 and #10 set for the buildings: geometry is rasterised once and thereafter only moved, scaled or faded, because every NeonDraw call expands into four antialiased polylines. The planet visuals never got that refactor. The black hole rebuilt ten glowing paths of twenty-five points every frame - forty polylines - to spin five rings; each ring is a NeonPiece that rotates now. The research lab spent nine draw calls a frame, eight of them fixed geometry, to turn one dish and blink six lights; the dish is a child that rotates and the lights a child whose modulate breathes. The gas giant's bands genuinely deform, so they still draw, but its constant rim moved out to a child. NeonPiece gained add_path, which it needed for open arcs. And I had broken the rule myself. ScannerGuidance re-queued a redraw of its arrow every frame for a colour that changes only with the destination - and it runs with PROCESS_MODE_ALWAYS, so it was re-tessellating through every dialog that stops the rest of the game. Guarded, and pinned by a test that counts CanvasItem's own draw signal: 36 rasterisations in 0.6s before, one after. Two smaller things from the same pass: the station menu drew its condition bar by hand where the ship lab already uses a ProgressBar node, so it uses one now and its _draw is gone; and station/upkeep refreshes were wired into _refresh_garrison(), which nothing but a test calls, so both menus showed whatever was true when they opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-08 11:35:45 +02:00
if definition != null and definition.map_color != _drawn_glow:
_drawn_glow = definition.map_color
indicator.glow_color = _drawn_glow
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