228 lines
7.6 KiB
GDScript
228 lines
7.6 KiB
GDScript
@icon("./assets/icon.svg")
|
|
|
|
@tool
|
|
|
|
## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue.
|
|
class_name DialogueLabel extends RichTextLabel
|
|
|
|
|
|
## Emitted for each letter typed out.
|
|
signal spoke(letter: String, letter_index: int, speed: float)
|
|
|
|
## Emitted when the player skips the typing of dialogue.
|
|
signal skipped_typing()
|
|
|
|
## Emitted when typing starts
|
|
signal started_typing()
|
|
|
|
## Emitted when typing finishes.
|
|
signal finished_typing()
|
|
|
|
## [Deprecated] No longer emitted.
|
|
signal paused_typing(duration: float)
|
|
|
|
|
|
## The action to press to skip typing.
|
|
@export var skip_action: StringName = &"ui_cancel"
|
|
|
|
## The speed with which the text types out.
|
|
@export var seconds_per_step: float = 0.02
|
|
|
|
## Automatically have a brief pause when these characters are encountered.
|
|
@export var pause_at_characters: String = ".?!"
|
|
|
|
## Don't auto pause if the character after the pause is one of these.
|
|
@export var skip_pause_at_character_if_followed_by: String = ")\""
|
|
|
|
## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br]
|
|
## Abbreviations are limitted to 5 characters in length [br]
|
|
## Does not support multi-period abbreviations (ex. "p.m.")
|
|
@export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"]
|
|
|
|
## The amount of time to pause when exposing a character present in `pause_at_characters`.
|
|
@export var seconds_per_pause_step: float = 0.3
|
|
|
|
var _already_mutated_indices: PackedInt32Array = []
|
|
|
|
|
|
## The current line of dialogue.
|
|
var dialogue_line:
|
|
set(value):
|
|
if value != dialogue_line:
|
|
dialogue_line = value
|
|
_update_text()
|
|
get:
|
|
return dialogue_line
|
|
|
|
## Whether the label is currently typing itself out.
|
|
var is_typing: bool = false:
|
|
set(value):
|
|
var is_finished: bool = _is_typing != value and value == false and visible_characters == get_total_character_count()
|
|
_is_typing = value
|
|
if is_finished:
|
|
finished_typing.emit()
|
|
get:
|
|
return _is_typing and not _is_awaiting_mutation
|
|
var _is_typing: bool = false
|
|
|
|
var _last_wait_index: int = -1
|
|
var _last_mutation_index: int = -1
|
|
var _waiting_seconds: float = 0
|
|
var _is_awaiting_mutation: bool = false
|
|
var _is_skipping_mutations: bool = false
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if _is_typing:
|
|
# Type out text
|
|
if visible_ratio < 1:
|
|
# See if we are waiting
|
|
if _waiting_seconds > 0:
|
|
_waiting_seconds = _waiting_seconds - delta
|
|
# If we are no longer waiting then keep typing
|
|
if _waiting_seconds <= 0:
|
|
_type_next(delta, _waiting_seconds)
|
|
else:
|
|
# Make sure any mutations at the end of the line get run
|
|
_mutate_inline_mutations(get_total_character_count())
|
|
is_typing = false
|
|
|
|
|
|
## Sets the label's text from the current dialogue line. Override if you want
|
|
## to do something more interesting in your subclass.
|
|
func _update_text() -> void:
|
|
text = dialogue_line.text
|
|
|
|
|
|
## Start typing out the text
|
|
func type_out() -> void:
|
|
_update_text()
|
|
visible_characters = 0
|
|
visible_ratio = 0
|
|
_waiting_seconds = 0
|
|
_last_wait_index = -1
|
|
_last_mutation_index = -1
|
|
_already_mutated_indices.clear()
|
|
|
|
is_typing = true
|
|
started_typing.emit()
|
|
|
|
# Allow typing listeners a chance to connect
|
|
await get_tree().process_frame
|
|
|
|
if get_total_character_count() == 0:
|
|
is_typing = false
|
|
elif seconds_per_step == 0:
|
|
_mutate_remaining_mutations()
|
|
visible_characters = get_total_character_count()
|
|
is_typing = false
|
|
|
|
|
|
## Stop typing out the text and jump right to the end
|
|
func skip_typing() -> void:
|
|
_mutate_remaining_mutations()
|
|
visible_characters = get_total_character_count()
|
|
is_typing = false
|
|
skipped_typing.emit()
|
|
|
|
|
|
# Type out the next character(s)
|
|
func _type_next(delta: float, seconds_needed: float) -> void:
|
|
if _is_awaiting_mutation: return
|
|
|
|
if visible_characters == get_total_character_count():
|
|
return
|
|
|
|
if _last_mutation_index != visible_characters:
|
|
_last_mutation_index = visible_characters
|
|
_mutate_inline_mutations(visible_characters)
|
|
if _is_awaiting_mutation: return
|
|
|
|
# Pause on characters like "."
|
|
var waiting_seconds: float = seconds_per_pause_step if _should_auto_pause() else 0
|
|
if _last_wait_index != visible_characters and waiting_seconds > 0:
|
|
_last_wait_index = visible_characters
|
|
_waiting_seconds += waiting_seconds
|
|
else:
|
|
visible_characters += 1
|
|
if visible_characters <= get_total_character_count():
|
|
spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters))
|
|
# See if there's time to type out some more in this frame
|
|
seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters))
|
|
if seconds_needed > delta:
|
|
_waiting_seconds += seconds_needed
|
|
else:
|
|
_type_next(delta, seconds_needed)
|
|
|
|
|
|
# Get the speed for the current typing position
|
|
func _get_speed(at_index: int) -> float:
|
|
var speed: float = 1
|
|
for index in dialogue_line.speeds:
|
|
if index > at_index:
|
|
return speed
|
|
speed = dialogue_line.speeds[index]
|
|
return speed
|
|
|
|
|
|
# Run any inline mutations that haven't been run yet
|
|
func _mutate_remaining_mutations() -> void:
|
|
_is_skipping_mutations = true
|
|
for i in range(visible_characters, get_total_character_count() + 1):
|
|
_mutate_inline_mutations(i)
|
|
_is_skipping_mutations = false
|
|
|
|
|
|
# Run any mutations at the current typing position
|
|
func _mutate_inline_mutations(index: int) -> void:
|
|
for inline_mutation in dialogue_line.inline_mutations:
|
|
# inline mutations are an array of arrays in the form of [character index, resolvable function]
|
|
if inline_mutation[0] > index:
|
|
return
|
|
if inline_mutation[0] == index and not _already_mutated_indices.has(index):
|
|
if _is_skipping_mutations:
|
|
Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
|
else:
|
|
_is_awaiting_mutation = true
|
|
await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
|
|
_is_awaiting_mutation = false
|
|
|
|
_already_mutated_indices.append(index)
|
|
|
|
|
|
# Determine if the current autopause character at the cursor should qualify to pause typing.
|
|
func _should_auto_pause() -> bool:
|
|
if visible_characters == 0: return false
|
|
|
|
var parsed_text: String = get_parsed_text()
|
|
|
|
# Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out
|
|
# Note: visible characters can be larger than parsed_text after a translation event
|
|
if visible_characters >= parsed_text.length(): return false
|
|
|
|
# Ignore pause characters if they are next to a non-pause character
|
|
if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split():
|
|
return false
|
|
|
|
# Ignore "." if it's between two numbers
|
|
if visible_characters > 3 and parsed_text[visible_characters - 1] == ".":
|
|
var possible_number: String = parsed_text.substr(visible_characters - 2, 3)
|
|
if str(float(possible_number)).pad_decimals(1) == possible_number:
|
|
return false
|
|
|
|
# Ignore "." if it's used in an abbreviation
|
|
# Note: does NOT support multi-period abbreviations (ex. p.m.)
|
|
if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".":
|
|
for abbreviation in skip_pause_at_abbreviations:
|
|
if visible_characters >= abbreviation.length():
|
|
var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length())
|
|
if previous_characters == abbreviation:
|
|
return false
|
|
|
|
# Ignore two non-"." characters next to each other
|
|
var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split()
|
|
if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters:
|
|
return false
|
|
|
|
return parsed_text[visible_characters - 1] in pause_at_characters.split()
|