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
This commit is contained in:
parent
54b883d545
commit
7d1a584c15
11 changed files with 350 additions and 78 deletions
|
|
@ -160,6 +160,14 @@ func _process(delta: float) -> void:
|
||||||
_refresh_lab()
|
_refresh_lab()
|
||||||
if transfer_menu.visible:
|
if transfer_menu.visible:
|
||||||
transfer_menu.refresh()
|
transfer_menu.refresh()
|
||||||
|
# Both of these move on their own while they are open - a transporter
|
||||||
|
# wears down, an office starves, the fleet changes under the bill - so
|
||||||
|
# they follow the same rule as the menus above: refreshed while visible,
|
||||||
|
# untouched when not.
|
||||||
|
if station_menu.visible:
|
||||||
|
station_menu.refresh()
|
||||||
|
if upkeep_menu.visible:
|
||||||
|
upkeep_menu.refresh()
|
||||||
if world_map.visible or _menu_is_open():
|
if world_map.visible or _menu_is_open():
|
||||||
_clear_detected_context()
|
_clear_detected_context()
|
||||||
return
|
return
|
||||||
|
|
@ -817,8 +825,6 @@ func _layout_hangar() -> void:
|
||||||
|
|
||||||
func _refresh_garrison() -> void:
|
func _refresh_garrison() -> void:
|
||||||
garrison_menu.refresh()
|
garrison_menu.refresh()
|
||||||
station_menu.refresh()
|
|
||||||
upkeep_menu.refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_planning() -> void:
|
func _refresh_planning() -> void:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ extends Node2D
|
||||||
# core stays genuinely empty (nothing drawn inside CORE_RADIUS), and an
|
# core stays genuinely empty (nothing drawn inside CORE_RADIUS), and an
|
||||||
# accretion disc of rotating arc segments - not closed rings - stands in
|
# accretion disc of rotating arc segments - not closed rings - stands in
|
||||||
# for a surface, brighter and thicker toward the centre.
|
# for a surface, brighter and thicker toward the centre.
|
||||||
|
#
|
||||||
|
# Each ring is a NeonPiece that is SPUN, not redrawn. Issues #6 and #10 made
|
||||||
|
# that the rule for the buildings and it applies here more than anywhere: the
|
||||||
|
# disc was ten glowing paths of twenty-five points re-tessellated every frame
|
||||||
|
# - forty antialiased polylines - to turn five rings at five different rates.
|
||||||
|
# Rotation is a node property, so now nothing is rasterised after the first
|
||||||
|
# frame and the disc turns for free.
|
||||||
|
|
||||||
var disc_phase: float = 0.0
|
var disc_phase: float = 0.0
|
||||||
|
|
||||||
|
|
@ -14,36 +21,54 @@ const RING_STEP: float = 16.0
|
||||||
const DISC_COLOR: Color = Color(0.55, 0.15, 0.85)
|
const DISC_COLOR: Color = Color(0.55, 0.15, 0.85)
|
||||||
const DISC_HOT_COLOR: Color = Color(0.9, 0.6, 1.0)
|
const DISC_HOT_COLOR: Color = Color(0.9, 0.6, 1.0)
|
||||||
|
|
||||||
|
# One per ring, with the rate and direction it turns at.
|
||||||
|
var _rings: Array = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
func configure(_shape_seed: int) -> void:
|
func configure(_shape_seed: int) -> void:
|
||||||
queue_redraw()
|
if _rings.is_empty():
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
func extractor_anchor() -> Vector2:
|
func extractor_anchor() -> Vector2:
|
||||||
return Vector2(0.0, -(CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP + 12.0))
|
return Vector2(0.0, -(CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP + 12.0))
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _build() -> void:
|
||||||
disc_phase += delta
|
for child in get_children():
|
||||||
queue_redraw()
|
child.queue_free()
|
||||||
|
_rings.clear()
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
for i in range(RING_COUNT):
|
for i in range(RING_COUNT):
|
||||||
var radius: float = CORE_RADIUS + float(i) * RING_STEP
|
var radius: float = CORE_RADIUS + float(i) * RING_STEP
|
||||||
# 1.0 at the innermost ring, fading to 0.0 at the outermost - drives
|
# 1.0 at the innermost ring, fading to 0.0 at the outermost - drives
|
||||||
# both colour (hotter inward) and stroke weight/brightness.
|
# both colour (hotter inward) and stroke weight/brightness.
|
||||||
var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1))
|
var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1))
|
||||||
var ring_color: Color = DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65)
|
var piece: NeonPiece = NeonPiece.create(self, Vector2.ZERO,
|
||||||
# Alternating spin direction per ring and a speed tied to t reads as
|
DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65))
|
||||||
|
# The two arcs are built at their resting angles; the ring node turns
|
||||||
|
# them. Alternating direction per ring and a speed tied to t reads as
|
||||||
# genuine turbulence rather than one disc rotating as a rigid whole.
|
# genuine turbulence rather than one disc rotating as a rigid whole.
|
||||||
var spin: float = disc_phase * (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0)
|
|
||||||
for arc_i in range(2):
|
for arc_i in range(2):
|
||||||
var start: float = spin + float(arc_i) * PI + 0.15
|
var start: float = float(arc_i) * PI + 0.15
|
||||||
var end: float = start + PI - 0.3
|
var end: float = start + PI - 0.3
|
||||||
var points := PackedVector2Array()
|
var points := PackedVector2Array()
|
||||||
var steps: int = 24
|
var steps: int = 24
|
||||||
for j in range(steps + 1):
|
for j in range(steps + 1):
|
||||||
var angle: float = lerpf(start, end, float(j) / float(steps))
|
var angle: float = lerpf(start, end, float(j) / float(steps))
|
||||||
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
||||||
NeonDraw.draw_glowing_path(self, points, ring_color, 0.5 + t * 0.5, 0.45 + t * 0.35)
|
piece.add_path(points, 0.5 + t * 0.5, 0.45 + t * 0.35)
|
||||||
|
_rings.append({
|
||||||
|
"piece": piece,
|
||||||
|
"rate": (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
disc_phase += delta
|
||||||
|
# One property per ring. Nothing is rasterised again.
|
||||||
|
for ring in _rings:
|
||||||
|
(ring.piece as NeonPiece).rotation = disc_phase * float(ring.rate)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
var gas_phase: float = 0.0
|
var gas_phase: float = 0.0
|
||||||
|
var _rim: NeonPiece
|
||||||
|
|
||||||
|
|
||||||
func configure(_shape_seed: int) -> void:
|
func configure(_shape_seed: int) -> void:
|
||||||
|
|
@ -16,12 +17,24 @@ func _process(delta: float) -> void:
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
# The bands genuinely deform - each point moves along its own sine - so they
|
||||||
var rim := PackedVector2Array()
|
# are the one thing here that has to be re-tessellated to animate. The rim
|
||||||
|
# does not move at all, so it is a NeonPiece child rasterised once rather
|
||||||
|
# than a thirty-two point polygon rebuilt every frame alongside them
|
||||||
|
# (issues #6/#10).
|
||||||
|
func _ensure_rim() -> void:
|
||||||
|
if _rim != null:
|
||||||
|
return
|
||||||
|
var points := PackedVector2Array()
|
||||||
for i in range(32):
|
for i in range(32):
|
||||||
var angle := TAU * float(i) / 32.0
|
var angle := TAU * float(i) / 32.0
|
||||||
rim.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
|
points.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
|
||||||
NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9)
|
_rim = NeonPiece.create(self, Vector2.ZERO, Color(0.4, 0.78, 1.0))
|
||||||
|
_rim.add_polygon(points, 0.9)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
_ensure_rim()
|
||||||
for row in [-62.0, -28.0, 8.0, 43.0, 73.0]:
|
for row in [-62.0, -28.0, 8.0, 43.0, 73.0]:
|
||||||
var band := PackedVector2Array()
|
var band := PackedVector2Array()
|
||||||
var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row))
|
var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row))
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@ extends Node2D
|
||||||
# and armoured rather than a natural silhouette (#63: this is an ENEMY
|
# and armoured rather than a natural silhouette (#63: this is an ENEMY
|
||||||
# installation, not a celestial body), with a rotating dish and blinking
|
# installation, not a celestial body), with a rotating dish and blinking
|
||||||
# warning lights to read as occupied/hostile at a glance.
|
# warning lights to read as occupied/hostile at a glance.
|
||||||
|
#
|
||||||
|
# Built out of NeonPiece children rather than redrawn each frame. Issues #6
|
||||||
|
# and #10 established that rule for the buildings: every NeonDraw call
|
||||||
|
# expands into four antialiased polylines, so re-running _draw() to advance
|
||||||
|
# an animation re-tessellates the whole thing. This visual was doing exactly
|
||||||
|
# that - nine draw calls a frame, of which eight were fixed geometry - to
|
||||||
|
# rotate one dish and fade six lights. Both are node properties: the dish is
|
||||||
|
# a child that spins, the lights are a child whose modulate breathes, and the
|
||||||
|
# hull is rasterised once and never touched again.
|
||||||
|
|
||||||
var phase: float = 0.0
|
var phase: float = 0.0
|
||||||
|
|
||||||
|
|
@ -14,42 +23,72 @@ const CORE_COLOR: Color = Color(0.9, 0.25, 0.18)
|
||||||
const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15)
|
const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15)
|
||||||
const DARK_CORE: Color = Color(0.14, 0.02, 0.02)
|
const DARK_CORE: Color = Color(0.14, 0.02, 0.02)
|
||||||
|
|
||||||
|
var _dish: NeonPiece
|
||||||
|
var _lights: NeonPiece
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
func configure(_shape_seed: int) -> void:
|
func configure(_shape_seed: int) -> void:
|
||||||
queue_redraw()
|
if _dish == null:
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
func extractor_anchor() -> Vector2:
|
func extractor_anchor() -> Vector2:
|
||||||
return Vector2(0.0, -(RADIUS + 40.0))
|
return Vector2(0.0, -(RADIUS + 40.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
for child in get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
# The armoured hull and its core: fixed, so drawn once.
|
||||||
|
var shell: NeonPiece = NeonPiece.create(self, Vector2.ZERO, HULL_COLOR)
|
||||||
|
shell.core_color = DARK_CORE
|
||||||
|
shell.add_polygon(_ring(RADIUS, 0.0), 0.85)
|
||||||
|
var core: NeonPiece = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
|
||||||
|
core.core_color = DARK_CORE
|
||||||
|
core.add_polygon(_ring(RADIUS * 0.55, PI / 6.0), 0.9)
|
||||||
|
|
||||||
|
# The dish, drawn along +X and then SPUN by rotating this node - the
|
||||||
|
# rotation is what used to cost a full re-tessellation every frame.
|
||||||
|
_dish = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
|
||||||
|
_dish.core_color = DARK_CORE
|
||||||
|
var tip: Vector2 = Vector2(RADIUS * 1.35, 0.0)
|
||||||
|
_dish.add_line(Vector2.ZERO, tip, 0.55, 0.8)
|
||||||
|
_dish.add_polygon(_circle(tip, 11.0), 1.0)
|
||||||
|
|
||||||
|
# The warning lights blink by fading the whole piece, not by redrawing
|
||||||
|
# six circles at a new alpha.
|
||||||
|
_lights = NeonPiece.create(self, Vector2.ZERO, WARNING_COLOR)
|
||||||
|
_lights.core_color = DARK_CORE
|
||||||
|
for i in range(6):
|
||||||
|
var angle: float = TAU * float(i) / 6.0
|
||||||
|
_lights.add_polygon(_circle(Vector2(cos(angle), sin(angle)) * RADIUS, 5.0), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _ring(radius: float, offset: float) -> PackedVector2Array:
|
||||||
|
var points: PackedVector2Array = PackedVector2Array()
|
||||||
|
for i in range(6):
|
||||||
|
var angle: float = TAU * float(i) / 6.0 + offset
|
||||||
|
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
func _circle(centre: Vector2, radius: float) -> PackedVector2Array:
|
||||||
|
var points: PackedVector2Array = PackedVector2Array()
|
||||||
|
for i in range(12):
|
||||||
|
var angle: float = TAU * float(i) / 12.0
|
||||||
|
points.append(centre + Vector2(cos(angle), sin(angle)) * radius)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
phase += delta
|
phase += delta
|
||||||
queue_redraw()
|
# Two node properties. Nothing is rasterised again.
|
||||||
|
if _dish != null:
|
||||||
|
_dish.rotation = phase * 0.6
|
||||||
func _draw() -> void:
|
if _lights != null:
|
||||||
var hull: PackedVector2Array = PackedVector2Array()
|
_lights.modulate.a = 0.4 + 0.6 * absf(sin(phase * 4.0))
|
||||||
for i in range(6):
|
|
||||||
var angle: float = TAU * float(i) / 6.0
|
|
||||||
hull.append(Vector2(cos(angle), sin(angle)) * RADIUS)
|
|
||||||
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE)
|
|
||||||
|
|
||||||
var inner: PackedVector2Array = PackedVector2Array()
|
|
||||||
for i in range(6):
|
|
||||||
var angle: float = TAU * float(i) / 6.0 + PI / 6.0
|
|
||||||
inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55)
|
|
||||||
NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE)
|
|
||||||
|
|
||||||
# A slow-rotating dish - reads as actively transmitting, not derelict.
|
|
||||||
var dish_angle: float = phase * 0.6
|
|
||||||
var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35
|
|
||||||
NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE)
|
|
||||||
NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE)
|
|
||||||
|
|
||||||
# Blinking hull-corner warning lights.
|
|
||||||
var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0))
|
|
||||||
for i in range(6):
|
|
||||||
var angle: float = TAU * float(i) / 6.0
|
|
||||||
var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS
|
|
||||||
NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE)
|
|
||||||
|
|
|
||||||
32
tests/_lab_planet_visual_preview.gd
Normal file
32
tests/_lab_planet_visual_preview.gd
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Preview, not a test - the research lab planet's own art, so the NeonPiece
|
||||||
|
# rebuild can be compared against what it replaced.
|
||||||
|
#
|
||||||
|
# bash tests/screenshot.sh res://tests/_lab_planet_visual_preview.tscn shots/lab_planet.png 20 720x720
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Both planet visuals that were rebuilt out of NeonPiece children, side by
|
||||||
|
# side, wound forward so their moving parts are off their rest positions -
|
||||||
|
# the dish turned, the lights mid-blink, the disc rings spun apart.
|
||||||
|
var lab: Node2D = load("res://planets/ResearchLabPlanetVisual.gd").new()
|
||||||
|
lab.position = Vector2(200.0, 360.0)
|
||||||
|
add_child(lab)
|
||||||
|
lab.configure(1)
|
||||||
|
lab.phase = 1.1
|
||||||
|
lab._process(0.0)
|
||||||
|
|
||||||
|
var hole: Node2D = load("res://planets/BlackHoleVisual.gd").new()
|
||||||
|
hole.position = Vector2(540.0, 360.0)
|
||||||
|
add_child(hole)
|
||||||
|
hole.configure(1)
|
||||||
|
hole.disc_phase = 6.0
|
||||||
|
hole._process(0.0)
|
||||||
|
|
||||||
|
var gas: Node2D = load("res://planets/GasGiantVisual.gd").new()
|
||||||
|
gas.position = Vector2(360.0, 620.0)
|
||||||
|
add_child(gas)
|
||||||
|
gas.configure(1)
|
||||||
|
gas.gas_phase = 2.0
|
||||||
|
gas._process(0.0)
|
||||||
6
tests/_lab_planet_visual_preview.tscn
Normal file
6
tests/_lab_planet_visual_preview.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/_lab_planet_visual_preview.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="LabPlanetVisualPreview" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
111
tests/test_guidance_marker_redraw.gd
Normal file
111
tests/test_guidance_marker_redraw.gd
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# The flight marker must not re-rasterise itself to point somewhere.
|
||||||
|
#
|
||||||
|
# Issues #6 and #10 set this rule for the rest of the game: geometry is drawn
|
||||||
|
# ONCE and thereafter moved, rotated, scaled or faded, because every NeonDraw
|
||||||
|
# call expands into four antialiased polylines and redrawing to animate
|
||||||
|
# re-tessellates all of them. The marker broke it - it re-queued a redraw
|
||||||
|
# every frame for a glow colour that only changes when the DESTINATION does -
|
||||||
|
# and it is the worst node in the game to get wrong, because it runs with
|
||||||
|
# PROCESS_MODE_ALWAYS: it kept working through every dialog that stops the
|
||||||
|
# rest of the game.
|
||||||
|
#
|
||||||
|
# Counted off CanvasItem's own `draw` signal rather than a counter of ours,
|
||||||
|
# so what is measured is real rasterisation.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. a marker tracking one destination settles - it stops redrawing
|
||||||
|
# B. it is genuinely still tracking (so this cannot pass by freezing)
|
||||||
|
# C. a destination of a different colour does redraw it, once
|
||||||
|
|
||||||
|
var state: Node
|
||||||
|
var scene: Node
|
||||||
|
var indicator: Control
|
||||||
|
var draws: int = 0
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
var settled_at: int = 0
|
||||||
|
var first_position: Vector2
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
state = root.get_node("GameState")
|
||||||
|
state.reset_new_game()
|
||||||
|
state.intro_active = false
|
||||||
|
state.reset_quests()
|
||||||
|
state.buildings.clear()
|
||||||
|
scene = load("res://world/Main.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
current_scene = scene
|
||||||
|
indicator = scene.get_node("UI/ScannerGuidance/Indicator")
|
||||||
|
indicator.draw.connect(func(): draws += 1)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if elapsed < 0.4:
|
||||||
|
return false
|
||||||
|
# A destination far off, so the marker is pinned to a screen edge
|
||||||
|
# and its position keeps being recomputed every frame.
|
||||||
|
state.scanner_target = {
|
||||||
|
"planet_id": "redraw:probe", "definition_id": "rocky",
|
||||||
|
"name": "Probe", "world_position": Vector2(90000.0, -40000.0),
|
||||||
|
}
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
# Let it take up the target and draw whatever it needs to.
|
||||||
|
if elapsed < 0.5:
|
||||||
|
return false
|
||||||
|
settled_at = draws
|
||||||
|
first_position = indicator.position
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 2
|
||||||
|
return false
|
||||||
|
2:
|
||||||
|
# --- A. thirty-odd frames of tracking, no rasterising ---------
|
||||||
|
if elapsed < 0.6:
|
||||||
|
return false
|
||||||
|
var while_tracking: int = draws - settled_at
|
||||||
|
if while_tracking > 1:
|
||||||
|
return _fail("the marker rasterised %d times in %.1fs of tracking one destination" \
|
||||||
|
% [while_tracking, elapsed])
|
||||||
|
# --- B. and it really was tracking ---------------------------
|
||||||
|
if not scene.get_node("UI/ScannerGuidance").visible:
|
||||||
|
return _fail("the marker was not even visible, so this proves nothing")
|
||||||
|
print("tracking one destination: %d redraws in %.1fs" % [while_tracking, elapsed])
|
||||||
|
|
||||||
|
# --- C. a different world does repaint it --------------------
|
||||||
|
var before: int = draws
|
||||||
|
state.scanner_target = {
|
||||||
|
"planet_id": "redraw:probe2", "definition_id": "gas_giant",
|
||||||
|
"name": "Probe Two", "world_position": Vector2(-70000.0, 55000.0),
|
||||||
|
}
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 3
|
||||||
|
settled_at = before
|
||||||
|
return false
|
||||||
|
3:
|
||||||
|
if elapsed < 0.4:
|
||||||
|
return false
|
||||||
|
var repaints: int = draws - settled_at
|
||||||
|
if repaints < 1:
|
||||||
|
return _fail("changing destination never recoloured the marker")
|
||||||
|
if repaints > 2:
|
||||||
|
return _fail("changing destination rasterised %d times, expected about one" % repaints)
|
||||||
|
print("changing destination repaints it %d time(s), then settles" % repaints)
|
||||||
|
|
||||||
|
print("TEST_GUIDANCE_MARKER_REDRAW: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_GUIDANCE_MARKER_REDRAW: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
|
@ -14,6 +14,13 @@ const EDGE_MARGIN: float = 34.0
|
||||||
@onready var target_label: Label = $TargetLabel
|
@onready var target_label: Label = $TargetLabel
|
||||||
|
|
||||||
var fleet: Node2D
|
var fleet: Node2D
|
||||||
|
# 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:
|
func _ready() -> void:
|
||||||
|
|
@ -53,8 +60,9 @@ func _process(_delta: float) -> void:
|
||||||
# authored colour rather than being forced into one.
|
# authored colour rather than being forced into one.
|
||||||
var definition: Resource = PLANET_DEFINITIONS.get_definition(
|
var definition: Resource = PLANET_DEFINITIONS.get_definition(
|
||||||
String(target.get("definition_id", "")))
|
String(target.get("definition_id", "")))
|
||||||
if definition != null:
|
if definition != null and definition.map_color != _drawn_glow:
|
||||||
indicator.glow_color = definition.map_color
|
_drawn_glow = definition.map_color
|
||||||
|
indicator.glow_color = _drawn_glow
|
||||||
indicator.queue_redraw()
|
indicator.queue_redraw()
|
||||||
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
|
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
|
||||||
target_label.text = "%s · %.1f SECTORS" % [
|
target_label.text = "%s · %.1f SECTORS" % [
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,25 @@ extends "res://ui/menus/ModalMenu.gd"
|
||||||
# it does, so the bar row below shows what each of them has left, worst first,
|
# it does, so the bar row below shows what each of them has left, worst first,
|
||||||
# which is the number that decides whether another needs building.
|
# which is the number that decides whether another needs building.
|
||||||
|
|
||||||
# One bar, wide, for the vessel closest to being spent - the one that decides
|
# One bar, for the vessel closest to being spent - the one that decides
|
||||||
# whether another needs building. The per-vessel figures are listed as TEXT
|
# whether another needs building. The per-vessel figures are listed as TEXT
|
||||||
# below it: a row of small bars was unreadable at the size a phone draws this
|
# beneath it: a row of small bars was unreadable at the size a phone draws
|
||||||
# menu, and "cycles left" is a number the player wants to actually read.
|
# this menu, and "cycles left" is a number the player wants to read.
|
||||||
const BAR_WIDTH: float = 420.0
|
#
|
||||||
const BAR_HEIGHT: float = 40.0
|
# A ProgressBar NODE rather than rects in _draw(), the way the ship lab draws
|
||||||
const BAR_TOP: float = 494.0
|
# its research progress. Issues #6/#10 set the rule for the rest of the game -
|
||||||
|
# geometry is rasterised once and thereafter only moved, scaled or recoloured
|
||||||
|
# - and a menu that refreshes while it is open would otherwise re-tessellate
|
||||||
|
# its bar every frame to move one edge.
|
||||||
const WORN_COLOR: Color = Color(1.0, 0.35, 0.25)
|
const WORN_COLOR: Color = Color(1.0, 0.35, 0.25)
|
||||||
const FRESH_COLOR: Color = Color(0.5, 1.0, 0.55)
|
const FRESH_COLOR: Color = Color(0.5, 1.0, 0.55)
|
||||||
const PLATE_COLOR: Color = Color(0.02, 0.02, 0.06, 0.9)
|
|
||||||
const FRAME_COLOR: Color = Color(0.85, 0.96, 1.0, 0.85)
|
|
||||||
|
|
||||||
@onready var title: Label = $TitleLabel
|
@onready var title: Label = $TitleLabel
|
||||||
@onready var assigned_label: Label = $AssignedLabel
|
@onready var assigned_label: Label = $AssignedLabel
|
||||||
@onready var status: Label = $StatusLabel
|
@onready var status: Label = $StatusLabel
|
||||||
@onready var condition_label: Label = $ConditionLabel
|
@onready var condition_label: Label = $ConditionLabel
|
||||||
@onready var condition_list: Label = $ConditionList
|
@onready var condition_list: Label = $ConditionList
|
||||||
|
@onready var condition_bar: ProgressBar = $ConditionBar
|
||||||
@onready var less_button: IconButton = $LessButton
|
@onready var less_button: IconButton = $LessButton
|
||||||
@onready var more_button: IconButton = $MoreButton
|
@onready var more_button: IconButton = $MoreButton
|
||||||
|
|
||||||
|
|
@ -89,6 +91,7 @@ func refresh() -> void:
|
||||||
var left: Array = GameState.station_condition(building_id)
|
var left: Array = GameState.station_condition(building_id)
|
||||||
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
|
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
|
||||||
var unit: String = "RUNS" if vessel_type == GameState.VESSEL_TRANSPORTER else "REPAIRS"
|
var unit: String = "RUNS" if vessel_type == GameState.VESSEL_TRANSPORTER else "REPAIRS"
|
||||||
|
condition_bar.visible = not left.is_empty()
|
||||||
if left.is_empty():
|
if left.is_empty():
|
||||||
condition_label.text = ""
|
condition_label.text = ""
|
||||||
condition_list.text = ""
|
condition_list.text = ""
|
||||||
|
|
@ -101,27 +104,11 @@ func refresh() -> void:
|
||||||
lines.append("%s %d %d / %d %s" % [
|
lines.append("%s %d %d / %d %s" % [
|
||||||
name.to_upper(), i + 1, int(left[i]), full, unit])
|
name.to_upper(), i + 1, int(left[i]), full, unit])
|
||||||
condition_list.text = "\n".join(lines)
|
condition_list.text = "\n".join(lines)
|
||||||
queue_redraw()
|
# Worst first, so this is the one about to go.
|
||||||
|
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
|
||||||
|
condition_bar.value = fraction
|
||||||
func _draw() -> void:
|
# The fill is recoloured rather than redrawn: a StyleBox is shared
|
||||||
if building_id == 0:
|
# geometry, and setting its colour does not re-tessellate anything.
|
||||||
return
|
var fill: StyleBoxFlat = condition_bar.get_theme_stylebox("fill")
|
||||||
# One bar per vessel standing here, worst first: what is left of the work
|
if fill != null:
|
||||||
# it was built to do. Drawn rather than built from nodes because the count
|
fill.bg_color = WORN_COLOR.lerp(FRESH_COLOR, fraction)
|
||||||
# changes as vessels arrive and wear out, and a row of bars is cheaper to
|
|
||||||
# draw than to keep a pool of Controls in step with.
|
|
||||||
var left: Array = GameState.station_condition(building_id)
|
|
||||||
if left.is_empty():
|
|
||||||
return
|
|
||||||
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
|
|
||||||
# The worst of them. station_condition() answers worst first.
|
|
||||||
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
|
|
||||||
var x: float = (size.x - BAR_WIDTH) * 0.5
|
|
||||||
draw_rect(Rect2(x - 3.0, BAR_TOP - 3.0, BAR_WIDTH + 6.0, BAR_HEIGHT + 6.0),
|
|
||||||
PLATE_COLOR)
|
|
||||||
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT),
|
|
||||||
Color(FRESH_COLOR.r, FRESH_COLOR.g, FRESH_COLOR.b, 0.16))
|
|
||||||
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH * fraction, BAR_HEIGHT),
|
|
||||||
WORN_COLOR.lerp(FRESH_COLOR, fraction))
|
|
||||||
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT), FRAME_COLOR, false, 2.0)
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,27 @@
|
||||||
[gd_scene load_steps=3 format=3]
|
[gd_scene load_steps=5 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon"]
|
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon"]
|
||||||
[ext_resource type="Script" path="res://ui/menus/StationMenu.gd" id="menu"]
|
[ext_resource type="Script" path="res://ui/menus/StationMenu.gd" id="menu"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_background"]
|
||||||
|
bg_color = Color(0.025, 0.045, 0.09, 0.92)
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(0.18, 0.48, 0.62, 0.9)
|
||||||
|
corner_radius_top_left = 8
|
||||||
|
corner_radius_top_right = 8
|
||||||
|
corner_radius_bottom_right = 8
|
||||||
|
corner_radius_bottom_left = 8
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_fill"]
|
||||||
|
bg_color = Color(0.5, 1, 0.55, 0.88)
|
||||||
|
corner_radius_top_left = 6
|
||||||
|
corner_radius_top_right = 6
|
||||||
|
corner_radius_bottom_right = 6
|
||||||
|
corner_radius_bottom_left = 6
|
||||||
|
|
||||||
[node name="StationMenu" type="Control" groups=["modal_menus"]]
|
[node name="StationMenu" type="Control" groups=["modal_menus"]]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
@ -83,6 +102,21 @@ theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
|
||||||
text = ""
|
text = ""
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ConditionBar" type="ProgressBar" parent="."]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = -210.0
|
||||||
|
offset_top = 494.0
|
||||||
|
offset_right = 210.0
|
||||||
|
offset_bottom = 534.0
|
||||||
|
theme_override_styles/background = SubResource("StyleBoxFlat_condition_background")
|
||||||
|
theme_override_styles/fill = SubResource("StyleBoxFlat_condition_fill")
|
||||||
|
max_value = 1.0
|
||||||
|
step = 0.001
|
||||||
|
show_percentage = false
|
||||||
|
|
||||||
[node name="ConditionList" type="Label" parent="."]
|
[node name="ConditionList" type="Label" parent="."]
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ extends Node2D
|
||||||
# owner can move, spin, scale and fade it without touching the shape.
|
# owner can move, spin, scale and fade it without touching the shape.
|
||||||
|
|
||||||
var polygons: Array = []
|
var polygons: Array = []
|
||||||
|
# Open runs of points, as distinct from `polygons`, which close. An accretion
|
||||||
|
# disc is arcs, not rings, so it needs the open form to be a piece at all.
|
||||||
|
var paths: Array = []
|
||||||
var lines: Array = []
|
var lines: Array = []
|
||||||
var glow_color: Color = Color.WHITE
|
var glow_color: Color = Color.WHITE
|
||||||
var core_color: Color = NeonDraw.DEFAULT_CORE
|
var core_color: Color = NeonDraw.DEFAULT_CORE
|
||||||
|
|
@ -35,6 +38,11 @@ func add_polygon(points: PackedVector2Array, piece_pulse: float = 1.0) -> void:
|
||||||
polygons.append({"points": points, "pulse": piece_pulse})
|
polygons.append({"points": points, "pulse": piece_pulse})
|
||||||
|
|
||||||
|
|
||||||
|
func add_path(points: PackedVector2Array, piece_pulse: float = 1.0,
|
||||||
|
width: float = 1.0) -> void:
|
||||||
|
paths.append({"points": points, "pulse": piece_pulse, "width": width})
|
||||||
|
|
||||||
|
|
||||||
func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0) -> void:
|
func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0) -> void:
|
||||||
lines.append({"from": from, "to": to, "width": width, "pulse": piece_pulse})
|
lines.append({"from": from, "to": to, "width": width, "pulse": piece_pulse})
|
||||||
|
|
||||||
|
|
@ -42,6 +50,9 @@ func add_line(from: Vector2, to: Vector2, width: float, piece_pulse: float = 1.0
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
for poly in polygons:
|
for poly in polygons:
|
||||||
NeonDraw.draw_glowing_polygon(self, poly.points, glow_color, poly.pulse * pulse, core_color)
|
NeonDraw.draw_glowing_polygon(self, poly.points, glow_color, poly.pulse * pulse, core_color)
|
||||||
|
for path in paths:
|
||||||
|
NeonDraw.draw_glowing_path(self, path.points, glow_color,
|
||||||
|
path.pulse * pulse, path.width, core_color)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
NeonDraw.draw_glowing_line(self, line.from, line.to, glow_color,
|
NeonDraw.draw_glowing_line(self, line.from, line.to, glow_color,
|
||||||
line.pulse * pulse, line.width, core_color)
|
line.pulse * pulse, line.width, core_color)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue