extends Control # #122: what the guide says during the prologue. # # A banner rather than a dialogue box: the player is flying while they read # it, so it sits across the top out of the way of the fleet and never takes # a press to dismiss. The SCRIPT decides when a line is done, not the player # - a prologue that waits for a tap is a prologue that can be sat in. # # Lines queue rather than replacing each other, so a beat that fires while # the previous line is still up does not silently eat it. # #125: lines were gone before they had been read. A held line is the # cheapest possible pacing control and the prologue leans on it heavily. const HOLD_SECONDS: float = 6.5 const FADE_SECONDS: float = 0.5 const TEXT_COLOR: Color = Color(0.82, 0.92, 1.0) const FRAME_COLOR: Color = Color(0.45, 0.68, 0.95) # #125: the panel was a plain rectangle with a line under it. These are the # pieces that make it read as a transmission window instead - the corners # are cut, the short edges are brackets rather than sides, and there is a # tick strip along the bottom. All of it is geometry rather than art, so it # scales with the banner and costs nothing. const CUT: float = 26.0 const BRACKET: float = 34.0 const INSET: float = 7.0 const TICK_COUNT: int = 24 const TICK_HEIGHT: float = 5.0 var label: Label var queued: Array = [] var current: String = "" var time_left: float = 0.0 func _ready() -> void: _ensure_label() # The director is a world node and this banner lives under the UI layer, # which readies afterwards - so the first line can arrive before _ready(). # Building on demand is simpler than making the director wait a frame, and # it cannot be got wrong twice. func _ensure_label() -> void: if label != null: return mouse_filter = Control.MOUSE_FILTER_IGNORE visible = false label = Label.new() label.mouse_filter = Control.MOUSE_FILTER_IGNORE label.add_theme_font_size_override("font_size", 30) label.add_theme_color_override("font_color", TEXT_COLOR) label.add_theme_color_override("font_outline_color", Color(0.01, 0.02, 0.06, 0.95)) label.add_theme_constant_override("outline_size", 4) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART add_child(label) resized.connect(_layout) _layout() func _layout() -> void: if label == null: return label.position = Vector2(24.0, 0.0) label.size = Vector2(maxf(size.x - 48.0, 0.0), size.y) # Says one line. Held for `hold` seconds, then the next in the queue. func say(line: String, hold: float = HOLD_SECONDS) -> void: _ensure_label() queued.append({"line": line, "hold": maxf(hold, 0.1)}) if current == "": _advance() func clear() -> void: queued.clear() current = "" time_left = 0.0 visible = false func is_speaking() -> bool: return current != "" or not queued.is_empty() func _advance() -> void: if queued.is_empty(): current = "" time_left = 0.0 visible = false return var next: Dictionary = queued.pop_front() current = String(next.line) time_left = float(next.hold) label.text = current visible = true queue_redraw() func _process(delta: float) -> void: if current == "": return time_left -= delta queue_redraw() if time_left <= 0.0: _advance() # The panel's outline: a rectangle with its corners cut off. Returned rather # than drawn so the fill and the stroke are guaranteed to be the same shape. func _frame() -> PackedVector2Array: var w: float = size.x var h: float = size.y return PackedVector2Array([ Vector2(CUT, 0.0), Vector2(w - CUT, 0.0), Vector2(w, CUT), Vector2(w, h - CUT), Vector2(w - CUT, h), Vector2(CUT, h), Vector2(0.0, h - CUT), Vector2(0.0, CUT), ]) func _draw() -> void: if current == "": return # Fades out over its last moments so lines hand over rather than cutting. var alpha: float = clampf(time_left / FADE_SECONDS, 0.0, 1.0) label.modulate.a = alpha var frame: PackedVector2Array = _frame() draw_colored_polygon(frame, Color(0.02, 0.05, 0.12, 0.78 * alpha)) NeonDraw.draw_glowing_polygon(self, frame, Color(FRAME_COLOR, alpha), 0.8) # Brackets at the short edges: a doubled inner line that stops well short # of the corners, so the panel reads as held open rather than closed. for side in [0.0, 1.0]: var x: float = lerpf(INSET, size.x - INSET, side) NeonDraw.draw_glowing_line(self, Vector2(x, CUT + INSET), Vector2(x, size.y - CUT - INSET), Color(FRAME_COLOR, alpha * 0.75), 0.6, 0.3) var arm: float = BRACKET * (1.0 if side < 0.5 else -1.0) for y in [CUT + INSET, size.y - CUT - INSET]: NeonDraw.draw_glowing_line(self, Vector2(x, y), Vector2(x + arm, y), Color(FRAME_COLOR, alpha * 0.75), 0.6, 0.3) # A tick strip along the bottom, dimming toward the ends. It is the one # piece that is pure texture, and it is what stops the lower edge from # reading as a plain underline. var span: float = size.x - CUT * 2.0 for i in range(TICK_COUNT): var t: float = float(i) / float(TICK_COUNT - 1) var tx: float = CUT + span * t var lit: float = 0.25 + 0.55 * sin(t * PI) draw_line(Vector2(tx, size.y - INSET), Vector2(tx, size.y - INSET - TICK_HEIGHT), Color(FRAME_COLOR, alpha * lit), 2.0, true) # And a caret in the top-left corner, so a line reads as being SAID by # someone rather than printed on the glass. NeonDraw.draw_glowing_path(self, PackedVector2Array([ Vector2(CUT + 4.0, INSET + 8.0), Vector2(CUT + 16.0, INSET + 15.0), Vector2(CUT + 4.0, INSET + 22.0), ]), Color(0.72, 0.92, 1.0, alpha * 0.9), 0.9, 0.4)