132 lines
4.8 KiB
GDScript3
132 lines
4.8 KiB
GDScript3
|
|
extends Control
|
||
|
|
|
||
|
|
# Forgejo #132: the standing objective, on the HUD.
|
||
|
|
#
|
||
|
|
# The banner (#128/#127) says a quest ONCE, stops the game while it is read,
|
||
|
|
# and then is gone. That is right for the sentence and wrong for the task -
|
||
|
|
# a player who put the game down mid-flight has nowhere to look up what they
|
||
|
|
# were doing. This is that place: small, always the current quest, and never
|
||
|
|
# in the way.
|
||
|
|
#
|
||
|
|
# Same idiom as the wave alert: a neon panel drawn out of geometry, no art,
|
||
|
|
# no textures. It sits under the alert on the right so the two stack rather
|
||
|
|
# than argue, and it stands down whenever a full-screen panel is open - the
|
||
|
|
# HUD's own rule for anything sitting behind a menu (see IconButton).
|
||
|
|
|
||
|
|
const GLOW: Color = Color(0.42, 0.86, 0.95)
|
||
|
|
const TEXT: Color = Color(0.80, 0.94, 1.0)
|
||
|
|
const DONE: Color = Color(0.55, 1.0, 0.68)
|
||
|
|
const CUT: float = 14.0
|
||
|
|
const PAD: float = 16.0
|
||
|
|
const TITLE_SIZE: int = 20
|
||
|
|
const OBJECTIVE_SIZE: int = 24
|
||
|
|
const BAR_HEIGHT: float = 8.0
|
||
|
|
|
||
|
|
var title_label: Label
|
||
|
|
var objective_label: Label
|
||
|
|
var count_label: Label
|
||
|
|
var progress: Vector2i = Vector2i.ZERO
|
||
|
|
var shown_id: String = ""
|
||
|
|
|
||
|
|
|
||
|
|
func _ready() -> void:
|
||
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
|
|
visible = false
|
||
|
|
title_label = _make_label(TITLE_SIZE, GLOW)
|
||
|
|
objective_label = _make_label(OBJECTIVE_SIZE, TEXT)
|
||
|
|
objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
|
|
count_label = _make_label(TITLE_SIZE, TEXT)
|
||
|
|
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
|
|
resized.connect(_layout)
|
||
|
|
_layout()
|
||
|
|
|
||
|
|
|
||
|
|
func _make_label(font_size: int, colour: Color) -> Label:
|
||
|
|
var label: Label = Label.new()
|
||
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
|
|
label.add_theme_font_size_override("font_size", font_size)
|
||
|
|
label.add_theme_color_override("font_color", colour)
|
||
|
|
label.add_theme_color_override("font_outline_color", Color(0.01, 0.03, 0.07, 0.95))
|
||
|
|
label.add_theme_constant_override("outline_size", 3)
|
||
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
|
|
add_child(label)
|
||
|
|
return label
|
||
|
|
|
||
|
|
|
||
|
|
func _layout() -> void:
|
||
|
|
if title_label == null:
|
||
|
|
return
|
||
|
|
var inner: float = maxf(size.x - PAD * 2.0, 0.0)
|
||
|
|
title_label.position = Vector2(PAD, PAD * 0.4)
|
||
|
|
title_label.size = Vector2(inner, float(TITLE_SIZE) * 1.3)
|
||
|
|
count_label.position = title_label.position
|
||
|
|
count_label.size = title_label.size
|
||
|
|
objective_label.position = Vector2(PAD, PAD * 0.4 + float(TITLE_SIZE) * 1.4)
|
||
|
|
objective_label.size = Vector2(inner,
|
||
|
|
maxf(size.y - objective_label.position.y - PAD - BAR_HEIGHT, 0.0))
|
||
|
|
|
||
|
|
|
||
|
|
# Open panels take the whole screen and this would sit on top of one. Same
|
||
|
|
# question IconButton._blocked_by_modal asks, for the same reason.
|
||
|
|
func _behind_a_panel() -> bool:
|
||
|
|
for menu in get_tree().get_nodes_in_group("modal_menus"):
|
||
|
|
if menu.is_visible_in_tree():
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
|
||
|
|
func _process(_delta: float) -> void:
|
||
|
|
var quest: Dictionary = GameState.active_quest()
|
||
|
|
var wanted: bool = not quest.is_empty() and not GameState.intro_active \
|
||
|
|
and not _behind_a_panel()
|
||
|
|
if not wanted:
|
||
|
|
if visible:
|
||
|
|
visible = false
|
||
|
|
return
|
||
|
|
var id: String = String(quest.id)
|
||
|
|
var now: Vector2i = GameState.quest_progress(quest)
|
||
|
|
if id == shown_id and now == progress and visible:
|
||
|
|
return
|
||
|
|
shown_id = id
|
||
|
|
progress = now
|
||
|
|
title_label.text = String(quest.get("title", ""))
|
||
|
|
objective_label.text = String(quest.get("objective", ""))
|
||
|
|
# A one-of-one goal is a yes/no, and "0 / 1" reads as a broken counter
|
||
|
|
# rather than as a task not started.
|
||
|
|
count_label.text = "%d / %d" % [progress.x, progress.y] if progress.y > 1 else ""
|
||
|
|
visible = true
|
||
|
|
queue_redraw()
|
||
|
|
|
||
|
|
|
||
|
|
func _frame() -> PackedVector2Array:
|
||
|
|
return PackedVector2Array([
|
||
|
|
Vector2(CUT, 0.0), Vector2(size.x, 0.0),
|
||
|
|
Vector2(size.x, size.y - CUT), Vector2(size.x - CUT, size.y),
|
||
|
|
Vector2(0.0, size.y), Vector2(0.0, CUT),
|
||
|
|
])
|
||
|
|
|
||
|
|
|
||
|
|
func _draw() -> void:
|
||
|
|
if not visible:
|
||
|
|
return
|
||
|
|
var frame: PackedVector2Array = _frame()
|
||
|
|
draw_colored_polygon(frame, Color(0.02, 0.07, 0.11, 0.66))
|
||
|
|
NeonDraw.draw_glowing_polygon(self, frame, Color(GLOW, 0.85), 0.75)
|
||
|
|
|
||
|
|
# A tick down the left edge, so the panel reads as clipped to the side of
|
||
|
|
# the screen rather than floating in the middle of it.
|
||
|
|
NeonDraw.draw_glowing_line(self, Vector2(PAD * 0.35, CUT + 4.0),
|
||
|
|
Vector2(PAD * 0.35, size.y - CUT - 4.0), Color(GLOW, 0.5), 0.6, 0.35)
|
||
|
|
|
||
|
|
# The bar along the bottom edge. Drawn for every goal, including the
|
||
|
|
# yes/no ones - it is the one thing on the panel that says "this is a
|
||
|
|
# task with a state" without needing to be read.
|
||
|
|
var track: Rect2 = Rect2(Vector2(PAD, size.y - PAD * 0.6 - BAR_HEIGHT),
|
||
|
|
Vector2(maxf(size.x - PAD * 2.0, 0.0), BAR_HEIGHT))
|
||
|
|
draw_rect(track, Color(GLOW, 0.16))
|
||
|
|
var filled: float = 0.0 if progress.y <= 0 \
|
||
|
|
else clampf(float(progress.x) / float(progress.y), 0.0, 1.0)
|
||
|
|
if filled <= 0.0:
|
||
|
|
return
|
||
|
|
var lit: Rect2 = Rect2(track.position, Vector2(track.size.x * filled, track.size.y))
|
||
|
|
draw_rect(lit, Color(DONE if filled >= 1.0 else GLOW, 0.85))
|