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))