#140: the objective moves behind a button next to the map's
The standing tracker #132 put on the HUD sat in the right-hand column, on top of the dev wave button - and there is no free strip of that edge to move it to, because everything down it is already something. So the objective stops living on the HUD: a quest button beside the map's, and a panel when you press it. The panel shows the current objective, its progress, and the road behind it. Deliberately 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. It owns its own button, the way WorldMap owns the map button rather than being wired through BuildController, and it hides that button during the prologue for the same reason and in the same two places (at _ready as well as in _process, since #128 means a dialog stops the tree before the first _process frame ever comes). Both scenes carry it. On a planet surface there is no map button to sit beside, so it goes under the planet's name. The test now checks the button is beside the map's and clear of the wave button by rectangle rather than by eye, which is the bug the ticket reported.
This commit is contained in:
parent
ac4b96ef49
commit
970eafc1de
7 changed files with 332 additions and 38 deletions
|
|
@ -57,6 +57,10 @@ enum Icon {
|
|||
PLANNING_CENTER,
|
||||
# #116: the workshop and the drone it sends out. Appended, same rule.
|
||||
WORKSHOP, REPAIR_DRONE,
|
||||
# #140: the standing objective. Appended, same rule as everything above -
|
||||
# inserting anywhere earlier renumbers every icon int already stored in
|
||||
# every scene.
|
||||
QUEST,
|
||||
}
|
||||
|
||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||
|
|
@ -313,6 +317,30 @@ func _draw() -> void:
|
|||
_draw_workshop_icon(center, pulse, s)
|
||||
Icon.REPAIR_DRONE:
|
||||
_draw_repair_drone_icon(center, pulse, s)
|
||||
Icon.QUEST:
|
||||
_draw_quest_icon(center, pulse, s)
|
||||
|
||||
|
||||
# #140: a checklist - a page with a tick against the top line. Reads as
|
||||
# "what I have been asked to do" rather than as a map, a target or a flag,
|
||||
# each of which this button sits next to.
|
||||
func _draw_quest_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||
var half: Vector2 = Vector2(9.0, 12.0) * s
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
center + Vector2(-half.x, -half.y), center + Vector2(half.x, -half.y),
|
||||
center + Vector2(half.x, half.y), center + Vector2(-half.x, half.y),
|
||||
center + Vector2(-half.x, -half.y),
|
||||
]), _active_color(), pulse, 0.5 * s, core_color)
|
||||
# The done line, ticked; the two below it still open.
|
||||
var tick: Vector2 = center + Vector2(-5.0, -5.0) * s
|
||||
NeonDraw.draw_glowing_path(self, PackedVector2Array([
|
||||
tick + Vector2(-1.5, 0.0) * s, tick + Vector2(0.5, 2.0) * s,
|
||||
tick + Vector2(3.5, -2.5) * s,
|
||||
]), _active_color(), pulse, 0.5 * s, core_color)
|
||||
for row in [1.0, 5.0]:
|
||||
NeonDraw.draw_glowing_line(self, center + Vector2(-5.5, row) * s,
|
||||
center + Vector2(5.5, row) * s, _active_color(), pulse * 0.75,
|
||||
0.4 * s, core_color)
|
||||
|
||||
|
||||
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
||||
|
|
|
|||
194
ui/QuestPanel.gd
Normal file
194
ui/QuestPanel.gd
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
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))
|
||||
1
ui/QuestPanel.gd.uid
Normal file
1
ui/QuestPanel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://buu5pg1783c6k
|
||||
Loading…
Reference in a new issue