194 lines
7 KiB
GDScript3
194 lines
7 KiB
GDScript3
|
|
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))
|