stick-the-quick/ui/Conversation/ConversationUI.gd

238 lines
6.1 KiB
GDScript

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