238 lines
6.1 KiB
GDScript3
238 lines
6.1 KiB
GDScript3
|
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()
|