class_name ConversationUI extends Control const STD_BG_COLOR := Color(0.125, 0.125, 0.25, 0.875) const CHARS_PER_SEC: float = 56.0 const PUNCT := { ',': { &'duration': 4.0, &'pitch': 1.05 }, ';': { &'duration': 8.0, &'pitch': 0.95 }, ':': { &'duration': 8.0, &'pitch': 0.95 }, '!': { &'duration': 12.0, &'pitch': 1.3 }, '?': { &'duration': 12.0, &'pitch': 1.2 }, '.': { &'duration': 12.0, &'pitch': 0.9 }, '-': { &'duration': 2.0, &'pitch': 1.1 }, '"': { &'duration': 1.0, &'pitch': 1.0 }, "'": { &'duration': 1.0, &'pitch': 1.0 } } const SILENT := { ' ': true, "\r": true, "\n": true, "\t": true } @onready var _message_stylebox := ( load('res://ui/Conversation/ConversationStyleBox.tres') ) as StyleBoxFlat @onready var _widget_stylebox := ( load('res://ui/Conversation/ConversationWidgetStyleBox.tres') ) as StyleBoxFlat @onready var _message_label := ( $'OuterMargin/VBoxContainer/MessageBox/Message' ) as RichTextLabel @onready var _continuation_arrow := ( $'OuterMargin/VBoxContainer/MessageBox/ContinuationArrow' ) @onready var _face_box := ( $'OuterMargin/VBoxContainer/Widgets/FaceBox' ) @onready var _face_widget := ( $'OuterMargin/VBoxContainer/Widgets/FaceBox/Face' ) as TextureRect @onready var _name_box := ( $'OuterMargin/VBoxContainer/Widgets/NameBox' ) @onready var _name_widget := ( $'OuterMargin/VBoxContainer/Widgets/NameBox/Name' ) as Label @onready var _choice_box := ( $'OuterMargin/VBoxContainer/Widgets/ChoiceBox' ) @onready var _choices_widget := ( $'OuterMargin/VBoxContainer/Widgets/ChoiceBox/Choices' ) as VBoxContainer @onready var _audio_player := ( $'AudioStreamPlayer' ) as AudioStreamPlayer var _blip: AudioStream var _speed: float = 1.0 var _timer: float = -1.0 var _timeout: float = 1.0/CHARS_PER_SEC var _any_choices := false var _default_choice := "OK" var _cancel_choice := "Cancel" var _message_finished := false var _skipped_to_end := false var message: String var options: Dictionary func _ready() -> void: options = UI.Args(self) message = options.get(&'message', "[Message missing]") _message_stylebox.bg_color = options.get(&'color', STD_BG_COLOR) _widget_stylebox.bg_color = options.get(&'color', STD_BG_COLOR) _message_stylebox.bg_color.a = options.get(&'alpha', STD_BG_COLOR.a) _widget_stylebox.bg_color.a = options.get(&'alpha', STD_BG_COLOR.a) if options.has(&'face'): _face_widget.texture = options.face _face_box.visible = true else: _face_box.visible = false if options.has(&'name'): _name_widget.text = options.name _name_box.visible = true else: _name_box.visible = false if options.has(&'choices'): var choices := options.choices as Array if not choices.is_empty(): _any_choices = true _default_choice = ( options.get(&'default_choice', choices[0]) ) _cancel_choice = ( options.get(&'cancel_choice', choices[choices.size() - 1]) ) for choice in choices: var elem := ( preload( 'res://ui/Conversation/ConversationChoice.tscn' ).instantiate() ) as ConversationChoice elem.text = choice _choices_widget.add_child(elem) elem.chosen.connect(_on_choice_chosen) _blip = options.get(&'blip', preload('res://audio/default_blip.ogg')) _audio_player.stream = _blip _speed = options.get(&'speed', 1.0) _continuation_arrow.visible = false _choice_box.visible = false _timeout /= _speed _message_label.text = message message = _message_label.get_parsed_text() _message_label.visible_characters = 0 func _process(delta: float) -> void: if not _message_finished: if _timer <= 0.0: var ch := message[_message_label.visible_characters] var ch2 := '' if _message_label.visible_characters < message.length() - 1: ch2 = message[_message_label.visible_characters + 1] if not SILENT.has(ch): _audio_player.stop() _audio_player.pitch_scale = ( PUNCT[ch].pitch if ( PUNCT.has(ch) and not PUNCT.has(ch2) ) else ( 1.0 + randf()/10.0 - 1.0/5.0 ) ) _audio_player.play() _timer = _timeout*( PUNCT[ch].duration if ( PUNCT.has(ch) and not PUNCT.has(ch2) ) else 1.0 ) _message_label.visible_characters += 1 if message.length() <= _message_label.visible_characters: _finish_message() _timer -= delta func _finish_message() -> void: _message_label.visible_characters = -1 _message_finished = true if _any_choices: _choice_box.visible = true for choice in _choices_widget.get_children(): if choice.text == _default_choice: choice._button.grab_focus() choice.selected.connect(_on_choice_selected) elif message[message.length() - 1] == '-' and not _skipped_to_end: UI.Return(self, true) else: _continuation_arrow.visible = true func _input(event: InputEvent) -> void: if not _choice_box: return elif ( event.is_action_released(&'confirm') or event.is_action_released(&'interact') ): if _message_finished: if _any_choices: var button := get_viewport().gui_get_focus_owner() var parent := button.get_parent() if button else null var grandparent := parent.get_parent() if parent else null var choice: String if grandparent and (grandparent is ConversationChoice): choice = grandparent.text else: choice = _default_choice get_viewport().set_input_as_handled() await _on_choice_chosen(choice) else: get_viewport().set_input_as_handled() UI.Return(self, true) else: _skipped_to_end = true _finish_message() get_viewport().set_input_as_handled() elif event.is_action_released(&'cancel'): if _message_finished: get_viewport().set_input_as_handled() if _any_choices: _choice_box = null UI.Return(self, _cancel_choice) else: UI.Return(self, false) else: _skipped_to_end = true _finish_message() get_viewport().set_input_as_handled() func _on_choice_chosen(which: String) -> void: _choice_box.queue_free() _choice_box = null _audio_player.stop() _audio_player.pitch_scale = 1.0 _audio_player.stream = preload('res://audio/choice.ogg') _audio_player.play() await _audio_player.finished UI.Return(self, which) func _on_choice_selected(_dont_care: String) -> void: _audio_player.stop() _audio_player.stream = preload('res://audio/menu_select.ogg') _audio_player.play()