spaceship/ui/QuestPanel.gd

194 lines
7 KiB
GDScript3
Raw Normal View History

extends Control
# Forgejo #140: "quests overlap the new wave button. create a quest button
# next to the map one."
#
# The standing tracker #132 put on the HUD sat in the right-hand column, on
# top of the wave button - and there is no free strip of that screen to move
# it to, because everything down that edge is already something. So the
# objective stops living on the HUD and lives behind a button, like the map:
# a mark next to the map's own, and a panel when you press it.
#
# Owned the way WorldMap owns its open button rather than wired through
# BuildController: the button does one thing and it is this panel's.
#
# What it shows is the objective and the road behind it - what is done, and
# what is being asked for now. NOT what comes next: the chain is meant to
# open the game up a step at a time (#133), and a list of everything still
# to come is that discovery given away on the first press.
const GLOW: Color = Color(0.42, 0.86, 0.95)
const TEXT: Color = Color(0.82, 0.94, 1.0)
const DONE: Color = Color(0.55, 1.0, 0.68)
const MUTED: Color = Color(0.52, 0.66, 0.76)
const PAD: float = 34.0
const TITLE_SIZE: int = 40
const HEADING_SIZE: int = 22
const QUEST_SIZE: int = 34
const OBJECTIVE_SIZE: int = 26
const DONE_SIZE: int = 22
const BAR_HEIGHT: float = 14.0
const ROW_HEIGHT: float = 34.0
@onready var open_button: IconButton = get_node("../QuestButton")
@onready var close_button: IconButton = get_node("CloseButton")
var title_label: Label
var heading_label: Label
var quest_label: Label
var objective_label: Label
var count_label: Label
var done_heading: Label
var done_labels: Array[Label] = []
var progress: Vector2i = Vector2i.ZERO
var bar_rect: Rect2 = Rect2()
func _ready() -> void:
visible = false
# Transparent to GUI input, like every other panel here - see
# IconButton._blocked_by_modal for why the buttons check instead.
mouse_filter = Control.MOUSE_FILTER_IGNORE
title_label = _make_label(TITLE_SIZE, TEXT)
heading_label = _make_label(HEADING_SIZE, GLOW)
quest_label = _make_label(QUEST_SIZE, GLOW)
objective_label = _make_label(OBJECTIVE_SIZE, TEXT)
objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
count_label = _make_label(HEADING_SIZE, TEXT)
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
done_heading = _make_label(HEADING_SIZE, GLOW)
if open_button != null:
open_button.pressed.connect(open)
if close_button != null:
close_button.pressed.connect(close)
resized.connect(_layout)
if open_button != null and GameState.intro_active:
# #128: a dialog stops the tree and the prologue opens on one, so the
# first _process frame never comes. Applied here too.
open_button.visible = false
_layout()
func _make_label(font_size: int, colour: Color) -> Label:
var 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 open() -> void:
_refresh()
visible = true
func close() -> void:
visible = false
func _layout() -> void:
if title_label == null:
return
var inner: float = maxf(size.x - PAD * 2.0, 0.0)
var y: float = PAD
title_label.position = Vector2(PAD, y)
title_label.size = Vector2(inner, float(TITLE_SIZE) * 1.4)
y += float(TITLE_SIZE) * 2.2
heading_label.position = Vector2(PAD, y)
heading_label.size = Vector2(inner, float(HEADING_SIZE) * 1.4)
y += float(HEADING_SIZE) * 1.8
quest_label.position = Vector2(PAD, y)
quest_label.size = Vector2(inner, float(QUEST_SIZE) * 1.3)
count_label.position = quest_label.position
count_label.size = quest_label.size
y += float(QUEST_SIZE) * 1.5
objective_label.position = Vector2(PAD, y)
objective_label.size = Vector2(inner, float(OBJECTIVE_SIZE) * 2.6)
y += float(OBJECTIVE_SIZE) * 2.9
bar_rect = Rect2(Vector2(PAD, y), Vector2(inner, BAR_HEIGHT))
y += BAR_HEIGHT * 3.5
done_heading.position = Vector2(PAD, y)
done_heading.size = Vector2(inner, float(HEADING_SIZE) * 1.4)
y += float(HEADING_SIZE) * 1.9
for label in done_labels:
label.position = Vector2(PAD + 12.0, y)
label.size = Vector2(inner - 12.0, ROW_HEIGHT)
y += ROW_HEIGHT
queue_redraw()
func _refresh() -> void:
var quest: Dictionary = GameState.active_quest()
title_label.text = "Objectives"
if quest.is_empty():
heading_label.text = "NOTHING OUTSTANDING"
quest_label.text = "The list has run out."
objective_label.text = "The search does not stop because there is nothing left to tick off."
count_label.text = ""
progress = Vector2i(1, 1)
else:
heading_label.text = "CURRENT OBJECTIVE %d OF %d" \
% [GameState.quest_index + 1, GameState.quest_count()]
quest_label.text = String(quest.title)
objective_label.text = String(quest.objective)
progress = GameState.quest_progress(quest)
# 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 ""
# The road behind. Deliberately no list of what is still to come.
var completed: int = GameState.quest_index
done_heading.text = "DONE (%d)" % completed
while done_labels.size() < completed:
var label: Label = _make_label(DONE_SIZE, DONE)
done_labels.append(label)
for i in range(done_labels.size()):
var shown: bool = i < completed
done_labels[i].visible = shown
if shown:
done_labels[i].text = " ✓ %s" % String(GameState.quest_at(i).title)
_layout()
func _process(_delta: float) -> void:
# #122: the prologue leaves the player movement and nothing else, and
# this is a button like any other. Owned here rather than by
# BuildController, the same way the map owns its own.
if open_button != null:
var offered: bool = not GameState.intro_active
if open_button.visible != offered:
open_button.visible = offered
if not offered and visible:
close()
if visible:
# Live, so ore arriving while the panel is open works the bar up
# instead of looking stuck.
_refresh()
func _draw() -> void:
if not visible:
return
draw_rect(Rect2(Vector2.ZERO, size), Color(0.01, 0.04, 0.08, 0.93))
NeonDraw.draw_glowing_path(self, PackedVector2Array([
Vector2(0.0, 0.0), Vector2(size.x, 0.0), Vector2(size.x, size.y),
Vector2(0.0, size.y), Vector2(0.0, 0.0),
]), Color(GLOW, 0.8), 0.8, 0.8)
# A rule under the title, so the header reads as a header.
var rule_y: float = PAD + float(TITLE_SIZE) * 1.6
NeonDraw.draw_glowing_line(self, Vector2(PAD, rule_y), Vector2(size.x - PAD, rule_y),
Color(GLOW, 0.5), 0.6, 0.4)
if bar_rect.size.x <= 0.0:
return
draw_rect(bar_rect, 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
draw_rect(Rect2(bar_rect.position, Vector2(bar_rect.size.x * filled, bar_rect.size.y)),
Color(DONE if filled >= 1.0 else GLOW, 0.85))