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 # #160: the rule under the title used to sit at the title's own baseline, # which is exactly where the window's close button is - the line ran straight # through it. The header band is now as tall as the close button, so the rule # clears it and the content starts below both. const HEADER_HEIGHT: float = 150.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) # Short of the close button, so a long title wraps rather than running # underneath it. title_label.size = Vector2(maxf(inner - 140.0, 0.0), float(TITLE_SIZE) * 1.4) y = HEADER_HEIGHT + PAD * 0.6 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 goes on, even with 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 = HEADER_HEIGHT 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))