1378 lines
52 KiB
GDScript
1378 lines
52 KiB
GDScript
# MadTalk Godot Plugin by Fernando Cosentino
|
|
# https://github.com/fbcosentino/godot-madtalk
|
|
#
|
|
# License: MIT
|
|
# (But if you can be so kind as to mention the original in your Readme in case
|
|
# you base any work on this, I would be very glad :] )
|
|
|
|
extends Node
|
|
|
|
|
|
signal dialog_acknowledged
|
|
signal text_display_completed
|
|
signal speaker_changed(previous_speaker_id, previous_speaker_variant, new_speaker_id, new_speaker_variant)
|
|
signal voice_clip_requested(speaker_id, clip_path)
|
|
|
|
signal dialog_started(sheet_name, sequence_id)
|
|
signal dialog_finished(sheet_name, sequence_id)
|
|
|
|
#warning-ignore:unused_signal
|
|
signal evaluate_custom_condition(custom_id, custom_data)
|
|
#warning-ignore:unused_signal
|
|
signal activate_custom_effect(custom_id, custom_data)
|
|
|
|
signal dialog_sequence_processed(sheet_name, sequence_id)
|
|
signal dialog_item_processed(sheet_name, sequence_id, item_index)
|
|
|
|
signal message_text_shown(speaker_id, speaker_variant, message_text, force_hiding)
|
|
|
|
signal menu_option_activated(option_index, option_id)
|
|
signal time_updated(datetime_dict)
|
|
|
|
# Requests the menu to be processed externally, if dialog_buttons_container is not set
|
|
# menu_options is an Array of DialogNodeOptionData
|
|
# The signal handler can process it as below:
|
|
# func _on_MadTalk_external_menu_requested(menu_options):
|
|
# for option in menu_options:
|
|
# print(option.text) # Prints the string for this option as written in the sheet
|
|
# One of the options can then be selected with:
|
|
# $MadTalk.select_menu_option( <numerical index in the menu_options array> )
|
|
signal external_menu_requested(menu_options: Array, options_metadata: Array)
|
|
|
|
signal dialog_aborted
|
|
|
|
# Your scene should have a Control-descendant node with all dialog controls
|
|
# inside. The top Control should be overlayed on top of all your visual elements
|
|
# so it can capture mouse events first. One way to acomplish this is to
|
|
# simply have it at the end of your scene tree child list, with "Full Rect"
|
|
# layout and the mouse filter set to "Stop". Your scene root node can be of any
|
|
# type (doesn't have to descend from Control). It can even be a Spatial in
|
|
# a normal 3D project
|
|
|
|
## Array containing the character data, one record per character
|
|
## All items in this array must be of type MTCharacterData
|
|
@export var ListOfCharacters: Array[MTCharacterData] = [] # (Array, Resource)
|
|
|
|
@export_group("Layout Nodes")
|
|
## This is the main control overlay used to show all dialog activity under
|
|
## MadTalk responsibility. Usually a Control with "Full Rect" layout and mouse
|
|
## filter set to "Stop", but other scenarios are possible at your discretion.
|
|
@export var DialogMainControl: NodePath
|
|
@onready var dialog_maincontrol: Control = get_node_or_null(DialogMainControl)
|
|
|
|
## The Control-descendant holding all the objects in the text box
|
|
## but not the menu. Menu must be able to become visible when this is hidden
|
|
## In most simple cases this can be the label itself (or a Panel holding it)
|
|
@export var DialogMessageBox: NodePath
|
|
@onready var dialog_messagebox: Control = get_node_or_null(DialogMessageBox)
|
|
|
|
## The RichTextLabel used to display dialog messages
|
|
@export var DialogMessageLabel: NodePath
|
|
@onready var dialog_messagelabel: Control = get_node_or_null(DialogMessageLabel)
|
|
|
|
@export_subgroup("Speaker")
|
|
|
|
## The Label or RichTextLabel used to display the speaker name
|
|
@export var DialogSpeakerLabel: NodePath
|
|
@onready var dialog_speakerlabel = get_node_or_null(DialogSpeakerLabel)
|
|
|
|
## The TextureRect for showing avatars
|
|
@export var DialogSpeakerAvatar: NodePath
|
|
@onready var dialog_speakeravatar = get_node_or_null(DialogSpeakerAvatar)
|
|
|
|
@export_subgroup("Menu")
|
|
|
|
## The Control-descendant holding the entire button menu, including containers,
|
|
## decorations, etc. Hiding this should be enough to leave no trace of the
|
|
## menu on screen
|
|
## Having a menu in the game is entirely optional and you can leave menu-related
|
|
## items unassigned if you don't use menus in the dialog system
|
|
@export var DialogButtonsMenu: NodePath
|
|
@onready var dialog_menu = get_node_or_null(DialogButtonsMenu)
|
|
|
|
## The container (usually VBoxContainer) which will hold the button instances
|
|
## directly. There must be nothing inside this node, this is the lowest
|
|
## hierarchy node in the customization/decoration branch of the scene tree, and
|
|
## buttons will be created as direct children of this node
|
|
## If this node is not assigned, menus can still be used externally via signals
|
|
## If this is not assigned and menu is also not handled externally, menu options
|
|
## will not work
|
|
@export var DialogButtonsContainer: NodePath
|
|
@onready var dialog_buttons_container = get_node_or_null(DialogButtonsContainer)
|
|
|
|
@export_subgroup("Menu/Visited Buttons")
|
|
|
|
## Color to modulate buttons after the option was already selected during
|
|
## this gameplay, but before this particular dialog. See also ModulateWhenVisitedInThisDialog
|
|
@export var ModulateWhenVisitedPreviously: Color = Color.WHITE
|
|
|
|
## Color to modulate buttons after the option was already selected since this
|
|
## particular dialog was started. In that case, ignores ModulateWhenVisitedPreviously,
|
|
## so you should set both to the same color if they should not be different
|
|
@export var ModulateWhenVisitedInThisDialog: Color = Color.WHITE
|
|
|
|
@export_subgroup("Menu/Custom Button for Menu")
|
|
## (Optional) The PackedScene file containing a button template used to build the menu.
|
|
## You do not need this set to use menus. MadTalk will use the default Button
|
|
## class if this field is left blank.
|
|
## If assigned, must have a signal without ambiguity for direct connection which is emitted
|
|
## only when the option is selected. Signal must have no arguments.
|
|
## Many actions sharing a same signal having different values for an argument
|
|
## (e.g. InputEvent) is not supported.
|
|
@export var DialogButtonSceneFile: PackedScene = null
|
|
|
|
## If DialogButtonSceneFile is assigned: name of property used to set the text when instancing the node
|
|
## (otherwise, leave as is)
|
|
@export var DialogButtonTextProperty: String = "text"
|
|
|
|
## If DialogButtonSceneFile is assigned: Signal name emitted when the option is confirmed
|
|
## (otherwise, leave as is)
|
|
@export var DialogButtonSignalName: String = "pressed"
|
|
|
|
## If DialogButtonSceneFile is assigned and this property is not blank:
|
|
## name of a bool property set to true if the dialog option this button refers to was
|
|
## already selected before during this gameplay (in this dialog or not), set to false otherwise
|
|
@export var DialogButtonVisitedProperty: String = ""
|
|
|
|
## If DialogButtonSceneFile is assigned and this property is not blank:
|
|
## name of a bool property set to true if the dialog option this button refers to was
|
|
## already selected before since this particular dialog started, set to false otherwise
|
|
@export var DialogButtonVisitedInThisDialogProperty: String = ""
|
|
|
|
@export_subgroup("")
|
|
|
|
@export_group("Animations")
|
|
|
|
@export_subgroup("Custom Fade-in Fade-out Animations")
|
|
|
|
## AnimationPlayer object used for fade-in and fade-out transition animations
|
|
## if not given, animations will simply be disabled and only show() and hide()
|
|
## will be used instead
|
|
@export var DialogAnimationPlayer: NodePath
|
|
@onready var dialog_anims = get_node_or_null(DialogAnimationPlayer)
|
|
|
|
# Below are animation names taken from the AnimationPlayer specified above.
|
|
# Make sure the fade out animations have valid information in the last frame
|
|
# If a track ends before the last frame, duplicate the last keyframe at (or
|
|
# after) the last frame. Applying the updates of only the last frame must be
|
|
# enough to reset the tracks to their faded-out states
|
|
|
|
|
|
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for dialog fade in - displays the DialogMainControl node entirely
|
|
@export var TransitionAnimationName_DialogFadeIn: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for dialog fade out - hides the DialogMainControl node entirely
|
|
@export var TransitionAnimationName_DialogFadeOut: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for message box fade in - displays the DialogMessageBox node
|
|
@export var TransitionAnimationName_MessageBoxFadeIn: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for message box fade out - hides the DialogMessageBox node
|
|
@export var TransitionAnimationName_MessageBoxFadeOut: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for menu fade in - displays the DialogButtonsMenu node entirely
|
|
@export var TransitionAnimationName_MenuFadeIn: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for menu fade out - hides the DialogButtonsMenu node entirely
|
|
@export var TransitionAnimationName_MenuFadeOut: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for message showing up - e.g. characters gradually being typed
|
|
@export var TransitionAnimationName_TextShow: String = ""
|
|
## (If DialogAnimationPlayer is assigned:)
|
|
## Animation for message disappearing
|
|
@export var TransitionAnimationName_TextHide: String = ""
|
|
|
|
@export_subgroup("Progressive Text")
|
|
|
|
|
|
## Automatically animate text tweening the Label's percent_visible property
|
|
@export var AnimateText: bool = true
|
|
@export var AnimatedTextMilisecondPerCharacter: float = 50.0
|
|
|
|
## The AudioStreamPlayer containing the sound effect to play when text is being
|
|
## shown (example, clicks, key press, beep, etc). If the sound should play
|
|
## repeatedly until text is complete (most common case), set the audio stream
|
|
## to loop in its import options, otherwise it will play only once.
|
|
@export var KeyPressAudioStreamPlayer: NodePath
|
|
@onready var sfx_key_press = get_node_or_null(KeyPressAudioStreamPlayer)
|
|
|
|
@export_subgroup("Animations Called From Effects")
|
|
|
|
|
|
## AnimationPlayer used to play effect animations, when using the effect "Play Animation and Wait"
|
|
@export var EffectsAnimationPlayer: NodePath
|
|
@onready var effects_anims = get_node_or_null(EffectsAnimationPlayer)
|
|
|
|
@export_subgroup("")
|
|
|
|
@export_group("Advanced/Message Formatting")
|
|
|
|
## Text to be inserted before every message (of every speaker). This will be inserted
|
|
## BEFORE any formatting, so you can use all available syntax in it, including BBCode
|
|
## and variable parsing,
|
|
## e.g.: [b][lb]b[rb][lb]color=yellow[rb]$npc[lb]/color[rb][lb]/b[rb]: [/b]
|
|
@export var TextPrefixForAllMessages := ""
|
|
|
|
## Text to be appended after every message (of every speaker). Similarly to
|
|
## Text Prefix For All Messages, this is appended BEFORE formatting, so all syntax
|
|
## is available. If you have BBCode tags left open in the prefix, you should close
|
|
## them here.
|
|
@export var TextSuffixForAllMessages := ""
|
|
|
|
@export_group("Options")
|
|
|
|
|
|
## When a sequence ends in a message item, and the sequence has options for a
|
|
## menu, after showing the message the user has to interact acknowledging the
|
|
## message before the menu is shown. Enabling this option will automatically
|
|
## show the menu with the last message.
|
|
@export var AutoShowMenuOnLastMessage := false
|
|
|
|
@export_subgroup("In-Game Date-Time")
|
|
|
|
## (Only relevant if you're using MadTalk to handle in-game date and time.)
|
|
## Year base is used to offset the calendar, datetime objects are referenced
|
|
## to year 0001, and developer can shift that to any year of convenience to
|
|
## match dates to weekdays and leap years. Check docs on github to understand
|
|
## how to use this. Default works fine.
|
|
@export var YearOfReference: int = 1970
|
|
|
|
@export_subgroup("Debug")
|
|
|
|
@export var EnableDebugOutput: bool = false
|
|
|
|
@export_subgroup("")
|
|
@export_group("")
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
class DialogCursor:
|
|
var sheet_name : String = ""
|
|
var sequence_id : int = 0
|
|
var item_index : int = 0
|
|
|
|
func _init(sheetname, sequenceid, _itemindex):
|
|
sheet_name = sheetname
|
|
sequence_id = sequenceid
|
|
|
|
|
|
|
|
|
|
|
|
# Dialog data - you can customize this if you want, but leaving the default
|
|
# should work just fine
|
|
var dialog_data = preload("res://addons/madtalk/runtime/madtalk_data.tres")
|
|
|
|
|
|
# For each sheet, the array index for each sequence ID is searched only once
|
|
# and the map is cached to avoid unnecessary lookup loops
|
|
# This variable holds the map
|
|
# Structure is:
|
|
# sheet_sequence_to_index = {
|
|
# "sheet_name": {
|
|
# <sequence_ID>: <index in sheet.nodes Array>,
|
|
# ...
|
|
# },
|
|
# ...
|
|
# }
|
|
var sheet_sequence_to_index = {}
|
|
|
|
# Dictionary mapping character ID to MTCharacterData
|
|
var character_data = {}
|
|
|
|
# If for some reason a dialog is fired when another one is still going on, the
|
|
# new one is added to the queue. Whenever a dialog ends, the queue is checked
|
|
# and fired if required
|
|
# Structure is:
|
|
# dialog_queue = [<list of DialogCursor items>]
|
|
var dialog_queue = []
|
|
|
|
# Flags tracking the state of dialog Control nodes
|
|
# we don't rely on properties (like `visible`) since the user might have a
|
|
# different logic to display or hide messages (including resizing UI or
|
|
# always-visible elements)
|
|
var dialog_maincontrol_active = false
|
|
var dialog_messagebox_active = false
|
|
var dialog_messagelabel_active = false
|
|
var dialog_menu_active = false
|
|
var dialog_on_text_progress = false
|
|
var last_speaker_id = "" # used to identify if speaker_id has just changed
|
|
var last_speaker_variant = ""
|
|
var last_message_item = null
|
|
var last_message_text = ""
|
|
|
|
# Stores Tween node for text animation if used
|
|
var animated_text_tween = null
|
|
|
|
# Holds the target callable to be called when
|
|
# evaluating custom conditions
|
|
var custom_condition_callable = null
|
|
|
|
# Holds the target callable to be called when
|
|
# activating custom effects
|
|
var custom_effect_callable = null
|
|
|
|
# Flags set when a request to abort or skip the dialog are issued
|
|
# The difference between them is: when a dialog is skipped, messages are not
|
|
# shown anymore, but all the dialog tree is still traversed, all conditions are
|
|
# checked, animations are played and effects take place. Aborting stops where it
|
|
# is. This is important since game logic can be critically based on those
|
|
# effects. E.g. if an effect in the end of a conversation spawns a boss,
|
|
# skipping the dialog still spanws the boss, while aborting doesn't.
|
|
# Both flags are always cleared when starting a dialog.
|
|
var is_abort_requested = false
|
|
var is_skip_requested = false
|
|
|
|
|
|
# Array mapping menu indices to the dialog IDs they connect to
|
|
# Mostly used when using external menus
|
|
var menu_connected_ids = []
|
|
|
|
# RandomNumberGenerator used for, well, random numbers
|
|
# Global is not used to avoid restricting from other uses
|
|
var rng = RandomNumberGenerator.new()
|
|
|
|
var msgparser = MessageCodeParser.new()
|
|
|
|
func debug_print(text: String) -> void:
|
|
if EnableDebugOutput:
|
|
print("MADTALK: "+text)
|
|
|
|
func bool_as_int(value):
|
|
return 0 if (value == 0) else 1
|
|
|
|
|
|
func _ready():
|
|
var condition_connection_array = get_signal_connection_list("evaluate_custom_condition")
|
|
if condition_connection_array.size() > 0:
|
|
custom_condition_callable = condition_connection_array[0]["callable"]
|
|
|
|
var effect_connection_array = get_signal_connection_list("activate_custom_effect")
|
|
if effect_connection_array.size() > 0:
|
|
custom_effect_callable = effect_connection_array[0]["callable"]
|
|
|
|
MadTalkGlobals.set_game_year(YearOfReference)
|
|
|
|
rng.randomize()
|
|
|
|
if (dialog_anims):
|
|
# Sanitizes the animation names ensuring we only have valid animations
|
|
|
|
if not dialog_anims is AnimationPlayer:
|
|
dialog_anims = null
|
|
|
|
else:
|
|
if not dialog_anims.has_animation(TransitionAnimationName_DialogFadeIn):
|
|
TransitionAnimationName_DialogFadeIn = ""
|
|
if not dialog_anims.has_animation(TransitionAnimationName_DialogFadeOut):
|
|
TransitionAnimationName_DialogFadeOut = ""
|
|
if not dialog_anims.has_animation(TransitionAnimationName_MenuFadeIn):
|
|
TransitionAnimationName_MenuFadeIn = ""
|
|
if not dialog_anims.has_animation(TransitionAnimationName_MenuFadeOut):
|
|
TransitionAnimationName_MenuFadeOut = ""
|
|
if not dialog_anims.has_animation(TransitionAnimationName_TextShow):
|
|
TransitionAnimationName_TextShow = ""
|
|
if not dialog_anims.has_animation(TransitionAnimationName_TextHide):
|
|
TransitionAnimationName_TextHide = ""
|
|
|
|
dialog_anims.connect("animation_finished", Callable(self, "_on_animation_finished"))
|
|
|
|
# Move animations to their respective faded-out states
|
|
# or hide dialog main control and menu
|
|
if TransitionAnimationName_DialogFadeOut != "":
|
|
dialog_anims.play(TransitionAnimationName_DialogFadeOut, -1, 1.0, true)
|
|
dialog_anims.advance(0)
|
|
else:
|
|
dialog_maincontrol.hide()
|
|
|
|
if TransitionAnimationName_MenuFadeOut != "":
|
|
dialog_anims.play(TransitionAnimationName_MenuFadeOut, -1, 1.0, true)
|
|
dialog_anims.advance(0)
|
|
else:
|
|
if dialog_menu:
|
|
dialog_menu.hide()
|
|
|
|
if TransitionAnimationName_TextHide != "":
|
|
dialog_anims.play(TransitionAnimationName_TextHide, -1, 1.0, true)
|
|
dialog_anims.advance(0)
|
|
|
|
for char_data_item in ListOfCharacters:
|
|
character_data[char_data_item.id] = char_data_item
|
|
|
|
if (not dialog_data) or (not dialog_data is DialogData):
|
|
# Unfortunately we have an invalid database, discard and make a new one
|
|
dialog_data = DialogData.new()
|
|
debug_print("Dialog data invalid, using a blank one instead")
|
|
|
|
#if AnimateText:
|
|
# animated_text_tween = get_tree().create_tween()
|
|
# #add_child(animated_text_tween)
|
|
#
|
|
# animated_text_tween.owner = self
|
|
# animated_text_tween.connect("tween_all_completed", Callable(self, "_on_animated_text_tween_completed"))
|
|
|
|
if dialog_messagelabel:
|
|
dialog_messagelabel.percent_visible = 0
|
|
|
|
MadTalkGlobals.is_during_dialog = false
|
|
await get_tree().process_frame
|
|
emit_signal("time_updated", MadTalkGlobals.gametime)
|
|
|
|
|
|
func _prepare_sheet_sequence_map(sheet_name, sequence_id) -> int:
|
|
# Check if we need to lookup and add this sheet/sequence to map
|
|
# Happens the first time it is accessed
|
|
if not sheet_name in sheet_sequence_to_index:
|
|
sheet_sequence_to_index[sheet_name] = {}
|
|
if not sequence_id in sheet_sequence_to_index[sheet_name]:
|
|
var found = false
|
|
for i in range(dialog_data.sheets[sheet_name].nodes.size()):
|
|
if dialog_data.sheets[sheet_name].nodes[i].sequence_id == sequence_id:
|
|
sheet_sequence_to_index[sheet_name][sequence_id] = i
|
|
found = true
|
|
break
|
|
if not found:
|
|
return FAILED
|
|
|
|
return OK
|
|
|
|
|
|
|
|
func _retrieve_sequence_data(sheet_name: String = "", sequence_id: int = 0) -> Resource:
|
|
if not sheet_name in dialog_data.sheets:
|
|
debug_print("Requested sheet \"%s\" which doesn't exist" % sheet_name)
|
|
return null
|
|
var sheet_data = dialog_data.sheets[sheet_name]
|
|
|
|
if (not sheet_name in sheet_sequence_to_index) or (not sequence_id in sheet_sequence_to_index[sheet_name]):
|
|
debug_print("Sequence ID %s not mapped in sheet \"%s\" when it should" % [sequence_id, sheet_name])
|
|
return null
|
|
var sequence_index = sheet_sequence_to_index[sheet_name][sequence_id]
|
|
|
|
if sequence_index >= sheet_data.nodes.size():
|
|
debug_print("Sequence index %s out of node range in sheet \"%s\" when it should" % [sequence_index, sheet_name])
|
|
return null
|
|
return sheet_data.nodes[sequence_index]
|
|
|
|
func _retrieve_item_data(sequence_data, item_index: int = 0) -> Resource:
|
|
if not sequence_data:
|
|
return null
|
|
|
|
if item_index >= sequence_data.items.size():
|
|
return null
|
|
return sequence_data.items[item_index]
|
|
|
|
|
|
|
|
func _anim_dialog_main_visible(show: bool = true) -> void:
|
|
if show:
|
|
# Show main dialog interface if not yet visible
|
|
if not dialog_maincontrol_active:
|
|
dialog_maincontrol_active = true
|
|
if TransitionAnimationName_DialogFadeIn != "":
|
|
dialog_anims.play(TransitionAnimationName_DialogFadeIn)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_maincontrol:
|
|
dialog_maincontrol.show()
|
|
|
|
|
|
|
|
else:
|
|
# Hide dialog box
|
|
if dialog_maincontrol_active:
|
|
if TransitionAnimationName_DialogFadeOut != "":
|
|
dialog_anims.play(TransitionAnimationName_DialogFadeOut)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_maincontrol:
|
|
dialog_maincontrol.hide()
|
|
|
|
dialog_maincontrol_active = false
|
|
|
|
|
|
func _anim_dialog_messagebox_visible(show: bool = true) -> void:
|
|
|
|
if show:
|
|
# Show message box
|
|
if not dialog_messagebox_active:
|
|
dialog_messagebox_active = true
|
|
if TransitionAnimationName_MessageBoxFadeIn != "":
|
|
dialog_anims.play(TransitionAnimationName_MessageBoxFadeIn)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_messagebox:
|
|
dialog_messagebox.show()
|
|
|
|
|
|
else:
|
|
# Hide message box
|
|
if dialog_messagebox_active:
|
|
if TransitionAnimationName_MessageBoxFadeOut != "":
|
|
dialog_anims.play(TransitionAnimationName_MessageBoxFadeOut)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_messagebox:
|
|
dialog_messagebox.hide()
|
|
|
|
dialog_messagebox_active = false
|
|
|
|
|
|
|
|
func _anim_dialog_text_visible(show: bool = true, percent_visible_range: Array= [0.0, 1.0], skip_animation: bool = false) -> void:
|
|
if show:
|
|
# Display animation always plays even if text is already visible
|
|
dialog_messagelabel_active = true
|
|
if AnimateText:
|
|
if TransitionAnimationName_TextShow != "":
|
|
dialog_anims.play(TransitionAnimationName_TextShow)
|
|
# If AnimateText is used, AnimationPlayer is not expected to
|
|
# handle text progression, so we wait the normal way
|
|
await dialog_anims.animation_finished
|
|
|
|
if not skip_animation:
|
|
|
|
if dialog_messagelabel:
|
|
dialog_on_text_progress = true
|
|
dialog_messagelabel.visible_ratio = percent_visible_range[0]
|
|
# Tween text progression
|
|
if animated_text_tween:
|
|
animated_text_tween.kill()
|
|
animated_text_tween = create_tween()
|
|
animated_text_tween.tween_property(dialog_messagelabel, "visible_ratio", percent_visible_range[1],
|
|
AnimatedTextMilisecondPerCharacter * dialog_messagelabel.text.length() * 0.001
|
|
).set_trans(Tween.TRANS_LINEAR)
|
|
animated_text_tween.tween_callback(_on_animated_text_tween_completed)
|
|
|
|
if sfx_key_press:
|
|
sfx_key_press.play()
|
|
await self.text_display_completed # both user interaction or animation_finished are routed here
|
|
if sfx_key_press:
|
|
sfx_key_press.stop()
|
|
dialog_on_text_progress = false
|
|
|
|
else:
|
|
if dialog_messagelabel:
|
|
dialog_messagelabel.percent_visible = 1.0
|
|
|
|
|
|
else:
|
|
if TransitionAnimationName_TextShow != "":
|
|
if not skip_animation:
|
|
dialog_on_text_progress = true
|
|
dialog_anims.play(TransitionAnimationName_TextShow)
|
|
await self.text_display_completed # both user interaction or animation_finished are routed here
|
|
dialog_on_text_progress = false
|
|
else:
|
|
dialog_anims.assigned_animation = TransitionAnimationName_TextShow
|
|
dialog_anims.seek(0)
|
|
dialog_anims.advance(dialog_anims.current_animation_length)
|
|
|
|
|
|
else:
|
|
if dialog_messagelabel_active:
|
|
if TransitionAnimationName_TextHide != "":
|
|
dialog_anims.play(TransitionAnimationName_TextHide)
|
|
await dialog_anims.animation_finished
|
|
dialog_messagelabel.visible_ratio = 0
|
|
await get_tree().process_frame
|
|
dialog_messagelabel_active = false
|
|
|
|
|
|
func _anim_dialog_menu_visible(show: bool = true) -> void:
|
|
if show:
|
|
# Menu is always regenerated when shown
|
|
# So animation is also always played
|
|
dialog_menu_active = true
|
|
if TransitionAnimationName_MenuFadeIn != "":
|
|
dialog_anims.play(TransitionAnimationName_MenuFadeIn)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_menu:
|
|
dialog_menu.show()
|
|
|
|
else:
|
|
if dialog_menu_active:
|
|
if TransitionAnimationName_MenuFadeOut != "":
|
|
dialog_anims.play(TransitionAnimationName_MenuFadeOut)
|
|
await dialog_anims.animation_finished
|
|
else:
|
|
if dialog_menu:
|
|
dialog_menu.hide()
|
|
|
|
dialog_menu_active = false
|
|
|
|
|
|
func _assemble_button(index: int, id: int, text: String, parent_node: Node, option_metadata: Dictionary) -> Node:
|
|
# Invalid metadata defaults to enabled button
|
|
var is_btn_enabled: bool = (option_metadata["enabled"] != false) if ("enabled" in option_metadata) else true
|
|
var is_btn_visited: bool = ("visited" in option_metadata) and (option_metadata["visited"])
|
|
var is_btn_visited_dialog: bool = ("visited_dialog" in option_metadata) and (option_metadata["visited_dialog"])
|
|
|
|
var is_custom_button = true if DialogButtonSceneFile else false # DialogButtonSceneFile is not boolean, is_custom_button is
|
|
|
|
var new_btn = DialogButtonSceneFile.instantiate() if DialogButtonSceneFile else Button.new()
|
|
|
|
parent_node.add_child(new_btn)
|
|
new_btn.set(DialogButtonTextProperty, text)
|
|
|
|
if is_custom_button:
|
|
if DialogButtonVisitedProperty != "":
|
|
new_btn.set(DialogButtonVisitedProperty, is_btn_visited)
|
|
if DialogButtonVisitedInThisDialogProperty != "":
|
|
new_btn.set(DialogButtonVisitedInThisDialogProperty, is_btn_visited_dialog)
|
|
|
|
if is_btn_visited_dialog:
|
|
new_btn.modulate *= ModulateWhenVisitedInThisDialog
|
|
elif is_btn_visited:
|
|
new_btn.modulate *= ModulateWhenVisitedPreviously
|
|
|
|
new_btn.disabled = not is_btn_enabled
|
|
|
|
# _on_menu_button_pressed() is used to multiplex all button signals into one
|
|
new_btn.connect(DialogButtonSignalName, _on_menu_button_pressed.bind(index, id))
|
|
|
|
return new_btn
|
|
|
|
|
|
func _assemble_menu(options: Array, options_metadata: Array) -> int:
|
|
# options = [<list of DialogNodeOptionData>]
|
|
# Fields:
|
|
# DialogNodeOptionData.text : String = ""
|
|
# DialogNodeOptionData.text_locales : Dictionary (locale String: text String)
|
|
# read via get_localized_text()
|
|
# DialogNodeOptionData.connected_to_id : int = -1
|
|
#
|
|
# options_metadata = [<list of metadata Dictionaries>]
|
|
# Fields:
|
|
# "enabled" : bool = true -> if this option can be selected
|
|
# "visited : bool -> if this option was already selected before during this gameplay
|
|
# "visited_dialog : -> if this option was already selected since starting this dialog
|
|
#
|
|
# options[index] and options_metadata[index] share the same index
|
|
|
|
if not dialog_buttons_container:
|
|
debug_print("Menu button container not set")
|
|
return 0
|
|
|
|
# Remove any previous buttons
|
|
var old_buttons = dialog_buttons_container.get_children()
|
|
for btn in old_buttons:
|
|
dialog_buttons_container.remove_child(btn)
|
|
btn.queue_free()
|
|
|
|
# Add new buttons
|
|
var count = 0
|
|
menu_connected_ids.clear()
|
|
#for option_item in options:
|
|
for i: int in range(options.size()):
|
|
var option_item: DialogNodeOptionData = options[i]
|
|
var item_text = option_item.get_localized_text() # option_item.text
|
|
menu_connected_ids.append(item_text)
|
|
var _new_btn = _assemble_button(i, option_item.connected_to_id, item_text, dialog_buttons_container, options_metadata[i])
|
|
count += 1
|
|
|
|
return count
|
|
|
|
|
|
func _check_option_condition(var_name: String, operator: String, given_value: String) -> bool:
|
|
var result = false
|
|
var var_value = MadTalkGlobals.get_variable(var_name, 0)
|
|
|
|
var value = given_value.to_float() if given_value.is_valid_float() else MadTalkGlobals.get_variable(given_value, 0)
|
|
|
|
match operator:
|
|
"=":
|
|
result = (var_value == value)
|
|
"!=":
|
|
result = (var_value != value)
|
|
">":
|
|
result = (var_value > value)
|
|
">=":
|
|
result = (var_value >= value)
|
|
"<":
|
|
result = (var_value < value)
|
|
"<=":
|
|
result = (var_value <= value)
|
|
_:
|
|
result = false
|
|
|
|
return result
|
|
|
|
|
|
func set_variable(variable_name: String, value) -> void:
|
|
MadTalkGlobals.set_variable(variable_name, value)
|
|
|
|
|
|
func get_variable(variable_name: String):
|
|
return MadTalkGlobals.get_variable(variable_name)
|
|
|
|
|
|
func start_dialog(sheet_name: String, sequence_id : int = 0) -> void:
|
|
if MadTalkGlobals.is_during_dialog:
|
|
dialog_queue.append( DialogCursor.new(sheet_name, sequence_id, 0) )
|
|
return
|
|
|
|
# Start processing dialog. This flag will cause any other calls to be queued
|
|
# Other in-game effects might also read this flag (such as pausing enemies)
|
|
MadTalkGlobals.is_during_dialog = true
|
|
# `yield` statements from now on are safe, even nested into method calls
|
|
# -----------------------------------------------------
|
|
|
|
is_abort_requested = false
|
|
is_skip_requested = false
|
|
|
|
MadTalkGlobals.reset_options_visited_dialog()
|
|
|
|
emit_signal("dialog_started", sheet_name, sequence_id)
|
|
|
|
# The "dialog_started" signal can be used to prevent some dialogs by
|
|
# skipping or aborting dialogs before they start
|
|
# This is useful when player repeats a level from a checkpoint, you still
|
|
# need the effects, but not the text, so skip still calls the method below
|
|
if (not is_abort_requested):
|
|
await run_dialog_sequence(sheet_name, sequence_id)
|
|
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
|
|
# Hide menu if needed
|
|
if dialog_menu_active:
|
|
await _anim_dialog_menu_visible(false)
|
|
|
|
# Hide text if needed
|
|
if dialog_messagelabel_active:
|
|
await _anim_dialog_text_visible(false)
|
|
|
|
# hide message box if needed
|
|
if dialog_messagebox_active:
|
|
await _anim_dialog_messagebox_visible(false)
|
|
|
|
# Hide dialog if needed
|
|
if dialog_maincontrol_active:
|
|
await _anim_dialog_main_visible(false)
|
|
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
|
|
# -----------------------------------------------------
|
|
# Stop processing dialog. Next calls will run immediately.
|
|
# There must be no `yield` statements from now on to the end of the method
|
|
MadTalkGlobals.is_during_dialog = false
|
|
|
|
# If something is queued, process it before anything else calls this again
|
|
if dialog_queue.size() > 0:
|
|
var dialog_cursor = dialog_queue.pop_front()
|
|
if dialog_cursor:
|
|
start_dialog(dialog_cursor.sheet_name, dialog_cursor.sequence_id)
|
|
|
|
|
|
func run_dialog_sequence(sheet_name: String, sequence_id : int = 0) -> void:
|
|
# Asking to run an invalid dialog fails silently
|
|
if not sheet_name in dialog_data.sheets:
|
|
await get_tree().process_frame
|
|
debug_print("Sheet \"%s\" not found" % sheet_name)
|
|
return
|
|
|
|
# Make sure we have the node mapped
|
|
if _prepare_sheet_sequence_map(sheet_name, sequence_id) == FAILED:
|
|
await get_tree().process_frame
|
|
debug_print("Mapping sheet \"%s\", sequence %s failed" % [sheet_name, str(sequence_id)])
|
|
return
|
|
|
|
await run_dialog_item(sheet_name, sequence_id)
|
|
|
|
emit_signal("dialog_sequence_processed", sheet_name, sequence_id)
|
|
|
|
|
|
|
|
func run_dialog_item(sheet_name: String = "", sequence_id: int = 0, item_index: int = 0) -> void:
|
|
|
|
var sequence_data : DialogNodeData = _retrieve_sequence_data(sheet_name, sequence_id)
|
|
var dialog_item : DialogNodeItemData = _retrieve_item_data(sequence_data, item_index)
|
|
var should_run_next_item := true
|
|
|
|
var is_last_item: bool = (item_index >= (sequence_data.items.size()-1))
|
|
var should_auto_progress_message: bool = (is_last_item and AutoShowMenuOnLastMessage and (sequence_data.options.size() > 0))
|
|
|
|
if is_abort_requested:
|
|
emit_signal("dialog_aborted")
|
|
|
|
elif sequence_data: # Sanity check
|
|
|
|
if dialog_item:
|
|
# We still have an item to process inside this sequence
|
|
|
|
match dialog_item.item_type:
|
|
DialogNodeItemData.ItemTypes.Message:
|
|
# dialog_item.message_speaker_id : String
|
|
# dialog_item.message_text : String
|
|
# message_text can use locale, so it is retrieved via
|
|
# dialog_item.get_localized_text()
|
|
|
|
# We show the message here, but we don't hide, since the
|
|
# player might want to re-read the last message when a set
|
|
# of options is presented in the end of the sequence
|
|
|
|
# Skipping a dialog before a message is shown prevents the
|
|
# messages from showing up. But if this sequence has a menu
|
|
# we have to show the last message, so we still assing all
|
|
# the values, we just don't play the show animations or
|
|
# wait for confirmation
|
|
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
|
|
# If text still on screen, hide text
|
|
await _anim_dialog_text_visible(false)
|
|
|
|
# if speaker has changed, we hide dialog to show again
|
|
if (dialog_item.message_speaker_id != last_speaker_id) or (dialog_item.message_speaker_variant != last_speaker_variant):
|
|
await _anim_dialog_messagebox_visible(false)
|
|
emit_signal("speaker_changed", last_speaker_id, last_speaker_variant, dialog_item.message_speaker_id, dialog_item.message_speaker_variant)
|
|
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
# Modify values
|
|
var speaker_name = character_data[dialog_item.message_speaker_id].name \
|
|
if (dialog_item.message_speaker_id in character_data) \
|
|
else dialog_item.message_speaker_id
|
|
|
|
if dialog_speakerlabel:
|
|
dialog_speakerlabel.text = speaker_name
|
|
|
|
var dialog_message_data = msgparser.process(
|
|
TextPrefixForAllMessages + dialog_item.get_localized_text() + TextSuffixForAllMessages,
|
|
MadTalkGlobals.variables
|
|
)
|
|
var dialog_message_text = dialog_message_data[0]
|
|
var dialog_message_anim_pause_percentages = dialog_message_data[1]
|
|
|
|
dialog_message_text = dialog_message_text.replace("$time", MadTalkGlobals.gametime["time"])
|
|
dialog_message_text = dialog_message_text.replace("$date_inv", MadTalkGlobals.gametime["date_inv"])
|
|
dialog_message_text = dialog_message_text.replace("$date", MadTalkGlobals.gametime["date"])
|
|
dialog_message_text = dialog_message_text.replace("$weekday", MTDefs.WeekdayNames[MadTalkGlobals.gametime["weekday"]] )
|
|
dialog_message_text = dialog_message_text.replace("$wday", MTDefs.WeekdayNamesShort[MadTalkGlobals.gametime["weekday"]] )
|
|
|
|
# Should be last replacement, to avoid things in speaker nane to be mistaken for formatting
|
|
dialog_message_text = dialog_message_text.replace("$speaker_id", dialog_item.message_speaker_id )
|
|
dialog_message_text = dialog_message_text.replace("$speaker_name", speaker_name )
|
|
|
|
if dialog_messagelabel:
|
|
dialog_messagelabel.text = dialog_message_text
|
|
|
|
if dialog_speakeravatar:
|
|
if (dialog_item.message_speaker_id in character_data):
|
|
# are we using a valid variant?
|
|
var char_variants = character_data[dialog_item.message_speaker_id].variants
|
|
if (dialog_item.message_speaker_variant != "") and (dialog_item.message_speaker_variant in char_variants) \
|
|
and (char_variants[dialog_item.message_speaker_variant] is Texture2D):
|
|
dialog_speakeravatar.texture = char_variants[dialog_item.message_speaker_variant]
|
|
# Otherwise use default avatar
|
|
else:
|
|
dialog_speakeravatar.texture = character_data[dialog_item.message_speaker_id].avatar
|
|
else:
|
|
dialog_speakeravatar.texture = null
|
|
|
|
|
|
if not is_skip_requested:
|
|
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
|
|
emit_signal("message_text_shown",
|
|
dialog_item.message_speaker_id,
|
|
dialog_item.message_speaker_variant,
|
|
dialog_message_text,
|
|
dialog_item.message_hide_on_end
|
|
)
|
|
|
|
# Show main dialog interface if not yet visible
|
|
await _anim_dialog_main_visible(true)
|
|
|
|
# Show message box if not visible yet
|
|
await _anim_dialog_messagebox_visible(true)
|
|
|
|
# Request voice clip to be played
|
|
# Signal is emitted even when clip path is blank, so the
|
|
# previous audio can be stopped if this is desired
|
|
emit_signal("voice_clip_requested", dialog_item.message_speaker_id, dialog_item.get_localized_voice_clip())
|
|
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
var previous_percent_visible = 0.0
|
|
|
|
# If there are no pauses, we will have
|
|
# dialog_message_anim_pause_percentages = [1.0]
|
|
|
|
# If skip was requested after we enter this match case,
|
|
# we just don't wait for user confirmation to dismiss
|
|
|
|
for percent_visible in dialog_message_anim_pause_percentages:
|
|
# If skip was requested between pauses, process here
|
|
if is_skip_requested or is_abort_requested:
|
|
break
|
|
|
|
# Show text
|
|
await _anim_dialog_text_visible(true,
|
|
[previous_percent_visible, percent_visible]
|
|
) # Handles animation skip internally
|
|
|
|
if dialog_messagelabel:
|
|
dialog_messagelabel.visible_ratio = percent_visible
|
|
previous_percent_visible = percent_visible
|
|
|
|
# Confirmation to dismiss the message
|
|
if (not is_skip_requested) and (not is_abort_requested) and (not should_auto_progress_message):
|
|
await self.dialog_acknowledged
|
|
|
|
|
|
if (dialog_item.message_hide_on_end != 0) or is_skip_requested:
|
|
# We hide this message box as explicitly requested
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
await _anim_dialog_text_visible(false)
|
|
await _anim_dialog_messagebox_visible(false)
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
# Else: we do not hide the message straight away as next step
|
|
# could be showing options. We hide when we leave the sequence
|
|
last_speaker_id = dialog_item.message_speaker_id
|
|
last_speaker_variant = dialog_item.message_speaker_variant
|
|
last_message_item = dialog_item
|
|
last_message_text = dialog_message_text
|
|
|
|
|
|
|
|
DialogNodeItemData.ItemTypes.Condition:
|
|
# dialog_item.condition_type : MTDefs.ConditionTypes
|
|
# dialog_item.condition_values : Array
|
|
# dialog_item.connected_to_id : int = -1
|
|
|
|
# Test the condition
|
|
var result = await evaluate_condition(dialog_item.condition_type, dialog_item.condition_values)
|
|
|
|
if not result:
|
|
# Condition failed, we have to branch out of this sequence
|
|
should_run_next_item = false
|
|
|
|
# If something is connected, we jump
|
|
# If nothing is connected, this simply means aboting
|
|
if dialog_item.connected_to_id > -1:
|
|
await run_dialog_sequence(sheet_name, dialog_item.connected_to_id)
|
|
|
|
|
|
|
|
DialogNodeItemData.ItemTypes.Effect:
|
|
# dialog_item.effect_type : MTDefs.EffectTypes
|
|
# dialog_item.effect_values : Array
|
|
|
|
# "Change sheet" effect is an exception and is implemented
|
|
# directly in this block since it is scope-dependant
|
|
if dialog_item.effect_type == MTDefs.EffectTypes.ChangeSheet:
|
|
var new_sheet_name = dialog_item.effect_values[0]
|
|
var new_sequence_id = dialog_item.effect_values[1]
|
|
|
|
# Jump to sheet if valid, aborting dialog otherwise
|
|
should_run_next_item = false
|
|
if new_sheet_name in dialog_data.sheets:
|
|
await run_dialog_sequence(new_sheet_name, new_sequence_id)
|
|
|
|
# Animation and custom effects are also exception since
|
|
# involves pausing the sequence until it finishes
|
|
elif dialog_item.effect_type == MTDefs.EffectTypes.WaitAnim:
|
|
var anim_name = dialog_item.effect_values[0]
|
|
# Animation must exist and not be loop
|
|
if effects_anims and (effects_anims.has_animation(anim_name)) and (
|
|
not effects_anims.get_animation(anim_name).loop
|
|
):
|
|
effects_anims.play(anim_name)
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
await effects_anims.animation_finished
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
elif dialog_item.effect_type == MTDefs.EffectTypes.Custom:
|
|
if custom_effect_callable:
|
|
var custom_id = dialog_item.effect_values[0]
|
|
var custom_data_array = MadTalkGlobals.split_string_autodetect_rn(dialog_item.effect_values[1])
|
|
|
|
#emit_signal("activate_custom_effect", custom_id, custom_data_array)
|
|
await custom_effect_callable.call(custom_id, custom_data_array)
|
|
|
|
else:
|
|
# All other effects have global scope and are
|
|
# implemented in a separate method
|
|
activate_effect(dialog_item.effect_type, dialog_item.effect_values)
|
|
|
|
_:
|
|
debug_print("Invalid item type for item %s in sequence ID %s at sheet \"%s\"" % [item_index, sequence_id, sheet_name])
|
|
|
|
|
|
emit_signal("dialog_item_processed", sheet_name, sequence_id, item_index)
|
|
|
|
# We don't check if item_index is the last one, since the first
|
|
# invalid index will be properly handled in following call
|
|
# causing the sequence to be gracefully concluded (see below)
|
|
if should_run_next_item:
|
|
await run_dialog_item(sheet_name, sequence_id, item_index + 1)
|
|
|
|
|
|
else: # All items processed
|
|
|
|
# Running an item_index higher than last valid one means we
|
|
# finished the item list and have to process the end of sequence
|
|
# This means showing options or routing to the "continue" ID
|
|
|
|
# We process menu options even if dialog_buttons_container is not
|
|
# assigned, as the menu might be handled externally via signals
|
|
|
|
# Even if we have options, some of them can be conditional, and
|
|
# it might be the case all of them are and no item is left to
|
|
# be shown at the menu. So we have to buffer a list
|
|
var options_to_show := []
|
|
var options_metadata := []
|
|
menu_connected_ids.clear()
|
|
|
|
for option_item: DialogNodeOptionData in sequence_data.options:
|
|
var conditions_passed := true
|
|
if option_item.is_conditional:
|
|
# Case 1: is auto-disable and already visited
|
|
match option_item.autodisable_mode:
|
|
option_item.AutodisableModes.RESET_ON_SHEET_RUN:
|
|
if (MadTalkGlobals.get_option_visited_dialog(option_item)):
|
|
conditions_passed = false
|
|
|
|
option_item.AutodisableModes.ALWAYS:
|
|
if (MadTalkGlobals.get_option_visited_global(option_item)):
|
|
conditions_passed = false
|
|
|
|
# Case 2: variable conditions
|
|
if conditions_passed:
|
|
conditions_passed = _check_option_condition(
|
|
option_item.condition_variable,
|
|
option_item.condition_operator,
|
|
option_item.condition_value
|
|
)
|
|
|
|
# We show button if it's active OR it should be shown anyway but as disabled
|
|
if (conditions_passed) or (option_item.inactive_mode == option_item.InactiveMode.DISABLED):
|
|
options_to_show.append(option_item)
|
|
menu_connected_ids.append(option_item.connected_to_id)
|
|
options_metadata.append({
|
|
"enabled": conditions_passed,
|
|
"visited": MadTalkGlobals.get_option_visited_global(option_item),
|
|
"visited_dialog": MadTalkGlobals.get_option_visited_dialog(option_item)
|
|
})
|
|
|
|
# Now options_to_show contains options to show (disabled or not)
|
|
# of which the ones in options_disabled should be disabled
|
|
|
|
# Process options and build menu only with remaining items
|
|
if options_to_show.size() > 0:
|
|
# When there are menu options, "continue" is not used
|
|
|
|
# If we skipped dialog, there are no visible messages. We show
|
|
# last one back
|
|
if is_skip_requested and (last_message_item.message_hide_on_end == 0):
|
|
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
|
|
emit_signal("message_text_shown",
|
|
last_message_item.message_speaker_id,
|
|
last_message_item.message_speaker_variant,
|
|
last_message_text,
|
|
last_message_item.message_hide_on_end
|
|
)
|
|
|
|
# Show main dialog interface if not yet visible
|
|
await _anim_dialog_main_visible(true)
|
|
|
|
# Show message box if not visible yet
|
|
await _anim_dialog_messagebox_visible(true)
|
|
|
|
# We do not play voice
|
|
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
# Show text
|
|
await _anim_dialog_text_visible(true, [0, 1], true) # skips to end
|
|
|
|
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
|
|
# Internal menu logic (via dialog_buttons_container)
|
|
if dialog_buttons_container:
|
|
# Make sure menu is not visible
|
|
if dialog_menu_active:
|
|
await _anim_dialog_menu_visible(false)
|
|
# Regenerate buttons
|
|
var __= _assemble_menu(options_to_show, options_metadata)
|
|
# Show menu
|
|
await _anim_dialog_menu_visible(true)
|
|
|
|
else:
|
|
external_menu_requested.emit(options_to_show, options_metadata)
|
|
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
if dialog_buttons_container:
|
|
# There is always at least one optiong there otherwise we
|
|
# would not be into this `if`
|
|
dialog_buttons_container.get_child(0).grab_focus()
|
|
|
|
# Wait for an option
|
|
# Selecting an option is mandatory and dialog halts until then
|
|
|
|
var option_res = await self.menu_option_activated
|
|
var option_index = option_res[0]
|
|
var option_id = option_res[1]
|
|
|
|
# Hide menu
|
|
if dialog_buttons_container:
|
|
MadTalkGlobals.is_during_cinematic = true
|
|
await _anim_dialog_menu_visible(false)
|
|
MadTalkGlobals.is_during_cinematic = false
|
|
|
|
if option_id > -1:
|
|
var selected_option: DialogNodeOptionData = options_to_show[option_index]
|
|
MadTalkGlobals.set_option_visited(selected_option, true)
|
|
|
|
# jumping to another sequence might also be same speaker
|
|
# so we don't hide anything yet
|
|
await run_dialog_sequence(sheet_name, option_id)
|
|
else:
|
|
last_speaker_id = ""
|
|
last_speaker_variant = ""
|
|
emit_signal("dialog_finished", sheet_name, sequence_id)
|
|
|
|
|
|
elif sequence_data.continue_sequence_id > -1:
|
|
# "continue" ID might also be same speaker so we don't hide anything yet
|
|
await run_dialog_sequence(sheet_name, sequence_data.continue_sequence_id)
|
|
|
|
else:
|
|
last_speaker_id = ""
|
|
last_speaker_variant = ""
|
|
emit_signal("dialog_finished", sheet_name, sequence_id)
|
|
|
|
else:
|
|
debug_print("Invalid sequence \"%s\" in sheet \"%s\"" % [sequence_id, sheet_name])
|
|
|
|
|
|
|
|
|
|
func evaluate_condition(condition_type, condition_values):
|
|
# Returns true if condition is met, false otherwise
|
|
# May or may not morph into coroutine, caller must check with:
|
|
# if result is GDScriptFunctionState:
|
|
# result = yield(result, "completed")
|
|
|
|
match condition_type:
|
|
MTDefs.ConditionTypes.Random:
|
|
var random_value = rng.randf_range(0.0, 100.0)
|
|
return (random_value < condition_values[0])
|
|
|
|
MTDefs.ConditionTypes.VarBool:
|
|
var var_value = bool_as_int(MadTalkGlobals.get_variable(condition_values[0], 0))
|
|
var expected_value = bool_as_int(condition_values[1])
|
|
return (var_value == expected_value)
|
|
|
|
MTDefs.ConditionTypes.VarAtLeast:
|
|
var var_value = float(MadTalkGlobals.get_variable(condition_values[0], 0.0))
|
|
return (var_value >= float(condition_values[1]))
|
|
|
|
MTDefs.ConditionTypes.VarUnder:
|
|
var var_value = float(MadTalkGlobals.get_variable(condition_values[0], 0.0))
|
|
return (var_value < float(condition_values[1]))
|
|
|
|
MTDefs.ConditionTypes.VarString:
|
|
var var_value = str(MadTalkGlobals.get_variable(condition_values[0], ""))
|
|
return (var_value == str(condition_values[1]))
|
|
|
|
MTDefs.ConditionTypes.Time:
|
|
var min_time = MadTalkGlobals.split_time(condition_values[0])
|
|
var target_min_time_float = MadTalkGlobals.time_to_float(min_time[0], min_time[1])
|
|
|
|
var max_time = MadTalkGlobals.split_time(condition_values[1])
|
|
var target_max_time_float = MadTalkGlobals.time_to_float(max_time[0], max_time[1])
|
|
|
|
var curr_time_float = MadTalkGlobals.time_to_float(
|
|
MadTalkGlobals.gametime["hour"],
|
|
MadTalkGlobals.gametime["minute"]
|
|
)
|
|
|
|
# Normal range - e.g. 6:00-18:00
|
|
if target_min_time_float < target_max_time_float:
|
|
return (curr_time_float >= target_min_time_float) and (curr_time_float <= target_max_time_float)
|
|
# Inverted range - e.g. 18:00-6:00
|
|
else:
|
|
return (curr_time_float >= target_min_time_float) or (curr_time_float <= target_max_time_float)
|
|
|
|
MTDefs.ConditionTypes.DayOfWeek:
|
|
var target_min_day = condition_values[0]
|
|
var target_max_day = condition_values[1]
|
|
var curr_day = MadTalkGlobals.gametime["weekday"]
|
|
|
|
# Normal range - e.g. Mon-Fri
|
|
if target_min_day < target_max_day:
|
|
return (curr_day >= target_min_day) and (curr_day <= target_max_day)
|
|
# Inverted range - e.g. Sat-Sun
|
|
else:
|
|
return (curr_day >= target_min_day) or (curr_day <= target_max_day)
|
|
|
|
MTDefs.ConditionTypes.DayOfMonth:
|
|
var target_min_day = condition_values[0]
|
|
var target_max_day = condition_values[1]
|
|
var curr_day = MadTalkGlobals.gametime["day"]
|
|
|
|
# Normal range - e.g. 14 - 21
|
|
if target_min_day < target_max_day:
|
|
return (curr_day >= target_min_day) and (curr_day <= target_max_day)
|
|
# Inverted range - e.g. 25 - 14
|
|
else:
|
|
return (curr_day >= target_min_day) or (curr_day <= target_max_day)
|
|
|
|
MTDefs.ConditionTypes.Date:
|
|
var target_min_day_month = MadTalkGlobals.split_date(condition_values[0])
|
|
var target_min_intdate = MadTalkGlobals.date_to_int(target_min_day_month[0], target_min_day_month[1], 1)
|
|
|
|
var target_max_day_month = MadTalkGlobals.split_date(condition_values[1])
|
|
var target_max_intdate = MadTalkGlobals.date_to_int(target_max_day_month[0], target_max_day_month[1], 1)
|
|
|
|
var curr_intdate = MadTalkGlobals.date_to_int(MadTalkGlobals.gametime["day"], MadTalkGlobals.gametime["month"], 1)
|
|
|
|
# Normal range - e.g. 15/02 - 25/03
|
|
if target_min_intdate < target_max_intdate:
|
|
return (curr_intdate >= target_min_intdate) and (curr_intdate <= target_max_intdate)
|
|
# Inverted range - e.g. 25/12 - 28/02
|
|
else:
|
|
return (curr_intdate >= target_min_intdate) or (curr_intdate <= target_max_intdate)
|
|
|
|
MTDefs.ConditionTypes.ElapsedFromVar:
|
|
var delta_time = float(condition_values[0])
|
|
var target_time = MadTalkGlobals.get_variable(condition_values[1], 0)
|
|
var delta_currently_elapsed = MadTalkGlobals.time - target_time
|
|
|
|
return (delta_currently_elapsed >= delta_time)
|
|
|
|
MTDefs.ConditionTypes.Custom:
|
|
if (not custom_condition_callable):
|
|
return false
|
|
|
|
var custom_id = condition_values[0]
|
|
var custom_data_array = MadTalkGlobals.split_string_autodetect_rn(condition_values[1])
|
|
|
|
var result = await custom_condition_callable.call(custom_id, custom_data_array)
|
|
|
|
if (result is int) or (result is float):
|
|
return (result != 0)
|
|
|
|
elif result is bool:
|
|
return result
|
|
|
|
else:
|
|
return false
|
|
|
|
_:
|
|
return false
|
|
|
|
|
|
func activate_effect(effect_type, effect_values):
|
|
match effect_type:
|
|
MTDefs.EffectTypes.ChangeSheet:
|
|
# This effect is an exception and is not implemented here
|
|
pass
|
|
|
|
MTDefs.EffectTypes.SetVariable:
|
|
MadTalkGlobals.set_variable(effect_values[0], float(effect_values[1]))
|
|
|
|
MTDefs.EffectTypes.AddVariable:
|
|
var old_value = float(MadTalkGlobals.get_variable(effect_values[0]))
|
|
MadTalkGlobals.set_variable(effect_values[0], old_value + float(effect_values[1]))
|
|
|
|
MTDefs.EffectTypes.RandomizeVariable:
|
|
var range_min = float(effect_values[1])
|
|
var range_max = float(effect_values[2])
|
|
MadTalkGlobals.set_variable(effect_values[0],
|
|
rng.randf_range(range_min, range_max)
|
|
)
|
|
|
|
MTDefs.EffectTypes.StampTime:
|
|
MadTalkGlobals.set_variable(effect_values[0], MadTalkGlobals.time)
|
|
|
|
MTDefs.EffectTypes.SpendMinutes:
|
|
MadTalkGlobals.time += int(round(float(effect_values[0]) * 60)) # value * 60s
|
|
MadTalkGlobals.update_gametime_dict()
|
|
emit_signal("time_updated", MadTalkGlobals.gametime)
|
|
|
|
MTDefs.EffectTypes.SpendDays:
|
|
MadTalkGlobals.time += int(round(float(effect_values[0]) * 24*60*60)) # value * 24h * 60m * 60s
|
|
MadTalkGlobals.update_gametime_dict()
|
|
emit_signal("time_updated", MadTalkGlobals.gametime)
|
|
|
|
MTDefs.EffectTypes.SkipToTime:
|
|
MadTalkGlobals.time = MadTalkGlobals.next_time_at_time(effect_values[0])
|
|
MadTalkGlobals.update_gametime_dict()
|
|
emit_signal("time_updated", MadTalkGlobals.gametime)
|
|
|
|
MTDefs.EffectTypes.SkipToWeekDay:
|
|
MadTalkGlobals.time = MadTalkGlobals.next_time_at_weekday(effect_values[0])
|
|
MadTalkGlobals.update_gametime_dict()
|
|
emit_signal("time_updated", MadTalkGlobals.gametime)
|
|
|
|
MTDefs.EffectTypes.Custom:
|
|
# This effect is an exception and is not implemented here
|
|
pass
|
|
|
|
|
|
func dialog_acknowledge():
|
|
# Called externally by UI to confirm a dialog message and progress dialog
|
|
if dialog_on_text_progress:
|
|
# This happened during text progression
|
|
if AnimateText:
|
|
if animated_text_tween:
|
|
animated_text_tween.kill()
|
|
#dialog_messagelabel.percent_visible = 1.0 # moved to run_dialog_item()
|
|
|
|
else:
|
|
if (dialog_anims.current_animation == TransitionAnimationName_TextShow) and dialog_anims.is_playing():
|
|
dialog_anims.advance(dialog_anims.current_animation_length - dialog_anims.current_animation_position)
|
|
|
|
emit_signal("text_display_completed")
|
|
elif not MadTalkGlobals.is_during_cinematic:
|
|
emit_signal("dialog_acknowledged")
|
|
|
|
func dialog_abort():
|
|
is_abort_requested = true
|
|
dialog_acknowledge()
|
|
|
|
func dialog_skip():
|
|
is_skip_requested = true
|
|
dialog_acknowledge()
|
|
|
|
func change_scene_to_file(scene_path: String) -> void:
|
|
# Convenience method giving access to get_tree().change_scene()
|
|
# as a node method in the scene tree
|
|
# This exists so you can connect signals from animation tracks in
|
|
# AnimationPlayer's directly to cause scene changes
|
|
var __= get_tree().change_scene_to_file(scene_path)
|
|
|
|
func select_menu_option(index: int):
|
|
if index < menu_connected_ids.size():
|
|
menu_option_activated.emit(index, menu_connected_ids[index])
|
|
|
|
|
|
func _on_animation_finished(anim_name):
|
|
if anim_name == TransitionAnimationName_TextShow:
|
|
emit_signal("text_display_completed")
|
|
|
|
func _on_animated_text_tween_completed():
|
|
emit_signal("text_display_completed")
|
|
|
|
|
|
func _on_menu_button_pressed(index, id):
|
|
menu_option_activated.emit(index, id)
|
|
|
|
func get_sheet_names():
|
|
return dialog_data.sheets.keys()
|