bcp de characters

This commit is contained in:
E250429F
2026-09-03 09:43:13 +02:00
parent a061aff066
commit 025a866eef
1175 changed files with 85837 additions and 286 deletions
+518
View File
@@ -0,0 +1,518 @@
class_name DialogicGameHandler
extends Node
## Class that is used as the Dialogic autoload.
## Autoload script that allows you to interact with all of Dialogic's systems:[br]
## - Holds all important information about the current state of Dialogic.[br]
## - Provides access to all the subsystems.[br]
## - Has methods to start/end timelines.[br]
## States indicating different phases of dialog.
enum States {
IDLE, ## Dialogic is awaiting input to advance.
REVEALING_TEXT, ## Dialogic is currently revealing text.
ANIMATING, ## Some animation is happening.
AWAITING_CHOICE, ## Dialogic awaits the selection of a choice
WAITING ## Dialogic is currently awaiting something.
}
## Flags indicating what to clear when calling [method clear].
enum ClearFlags {
FULL_CLEAR = 0, ## Clears all subsystems
KEEP_VARIABLES = 1, ## Clears all subsystems and info except for variables
TIMELINE_INFO_ONLY = 2 ## Doesn't clear subsystems but current timeline and index
}
## Reference to the currently executed timeline.
var current_timeline: DialogicTimeline = null
## Copy of the [member current_timeline]'s events.
var current_timeline_events: Array = []
## Index of the event the timeline handling is currently at.
var current_event_idx: int = 0
## Current state (see [member States] enum).
var current_state := States.IDLE:
get:
return current_state
set(new_state):
current_state = new_state
state_changed.emit(new_state)
## Emitted when [member current_state] change.
signal state_changed(new_state:States)
## When `true`, many dialogic processes won't continue until it's `false` again.
var paused := false:
set(value):
paused = value
if paused:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem)._pause()
dialogic_paused.emit()
else:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem)._resume()
dialogic_resumed.emit()
## A timeline that will be played when dialog ends.
## By default this timeline only contains a clear event.
var dialog_ending_timeline: DialogicTimeline
## Emitted when [member paused] changes to `true`.
signal dialogic_paused
## Emitted when [member paused] changes to `false`.
signal dialogic_resumed
## Emitted when a timeline starts by calling either [method start]
## or [method start_timeline].
signal timeline_started
## Emitted when the timeline ends.
## This can be a timeline ending or [method end_timeline] being called.
signal timeline_ended
## Emitted when an event starts being executed.
## The event may not have finished executing yet.
signal event_handled(resource: DialogicEvent)
## Emitted when a [class SignalEvent] event was reached.
@warning_ignore("unused_signal") # This is emitted by the signal event.
signal signal_event(argument: Variant)
## Emitted when a signal event gets fired from a [class TextEvent] event.
@warning_ignore("unused_signal") # This is emitted by the text subsystem.
signal text_signal(argument: String)
# Careful, this section is repopulated automatically at certain moments.
#region SUBSYSTEMS
const AnimationSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd")
var Animations: AnimationSubsystem:
get: return get_subsystem("Animations")
const AudioSubsystem = preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd")
var Audio: AudioSubsystem:
get: return get_subsystem("Audio")
const BackgroundsSubsystem = preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd")
var Backgrounds: BackgroundsSubsystem:
get: return get_subsystem("Backgrounds")
const ChoicesSubsystem = preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd")
var Choices: ChoicesSubsystem:
get: return get_subsystem("Choices")
const ExpressionSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd")
var Expressions: ExpressionSubsystem:
get: return get_subsystem("Expressions")
const GlossarySubsystem = preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd")
var Glossary: GlossarySubsystem:
get: return get_subsystem("Glossary")
const HistorySubsystem = preload("res://addons/dialogic/Modules/History/subsystem_history.gd")
var History: HistorySubsystem:
get: return get_subsystem("History")
const InputsSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_input.gd")
var Inputs: InputsSubsystem:
get: return get_subsystem("Inputs")
const JumpSubsystem = preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd")
var Jump: JumpSubsystem:
get: return get_subsystem("Jump")
const PortraitContainersSubsystem = preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd")
var PortraitContainers: PortraitContainersSubsystem:
get: return get_subsystem("PortraitContainers")
const PortraitsSubsystem = preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd")
var Portraits: PortraitsSubsystem:
get: return get_subsystem("Portraits")
const SaveSubsystem = preload("res://addons/dialogic/Modules/Save/subsystem_save.gd")
var Save: SaveSubsystem:
get: return get_subsystem("Save")
const SettingsSubsystem = preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd")
var Settings: SettingsSubsystem:
get: return get_subsystem("Settings")
const StylesSubsystem = preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd")
var Styles: StylesSubsystem:
get: return get_subsystem("Styles")
const TextSubsystem = preload("res://addons/dialogic/Modules/Text/subsystem_text.gd")
var Text: TextSubsystem:
get: return get_subsystem("Text")
const TextInputSubsystem = preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd")
var TextInput: TextInputSubsystem:
get: return get_subsystem("TextInput")
const VARSubsystem = preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd")
var VAR: VARSubsystem:
get: return get_subsystem("VAR")
const VoiceSubsystem = preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd")
var Voice: VoiceSubsystem:
get: return get_subsystem("Voice")
const WaitSubsystem = preload("res://addons/dialogic/Modules/Wait/subsystem_wait.gd")
var Wait: WaitSubsystem:
get: return get_subsystem("Wait")
#endregion
## Autoloads are added first, so this happens REALLY early on game startup.
func _ready() -> void:
_collect_subsystems()
clear()
DialogicResourceUtil.update_event_cache()
dialog_ending_timeline = DialogicTimeline.new()
dialog_ending_timeline.from_text("[clear]")
if OS.is_debug_build():
instantiate_debug_overlay()
#region TIMELINE & EVENT HANDLING
################################################################################
## Method to start a timeline AND ensure that a layout scene is present.
## For argument info, checkout [method start_timeline].
## -> returns the layout node
func start(timeline:Variant, label_or_idx:Variant="") -> Node:
# If we don't have a style subsystem, default to just start_timeline()
if not has_subsystem("Styles"):
printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!")
clear(ClearFlags.KEEP_VARIABLES)
start_timeline(timeline, label_or_idx)
return null
# Otherwise make sure there is a style active.
var scene: Node = null
if not self.Styles.has_active_layout_node():
scene = self.Styles.load_style()
else:
scene = self.Styles.get_layout_node()
scene.show()
if not scene.is_node_ready():
if not scene.ready.is_connected(clear.bind(ClearFlags.KEEP_VARIABLES)):
scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES))
if not scene.ready.is_connected(start_timeline.bind(timeline, label_or_idx)):
scene.ready.connect(start_timeline.bind(timeline, label_or_idx))
else:
start_timeline(timeline, label_or_idx)
return scene
## Method to start a timeline without adding a layout scene.
## [param timeline] can be either a loaded timeline resource or a path to a timeline file.
## [param label_or_idx] can be a label (string) or index (int) to skip to immediatly.
func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void:
# load the resource if only the path is given
if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]:
#check the lookup table if it's not a full file name
if "://" in timeline:
timeline = load(timeline)
else:
timeline = DialogicResourceUtil.get_timeline_resource(timeline)
if timeline == null:
printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors")
return
(timeline as DialogicTimeline).process()
current_timeline = timeline
current_timeline_events = current_timeline.events
for event in current_timeline_events:
event.dialogic = self
current_event_idx = -1
if typeof(label_or_idx) in [TYPE_STRING, TYPE_STRING_NAME]:
if label_or_idx:
if has_subsystem("Jump"):
Jump.jump_to_label((label_or_idx as String))
elif typeof(label_or_idx) == TYPE_INT:
if label_or_idx >-1:
current_event_idx = label_or_idx -1
if not current_timeline == dialog_ending_timeline:
timeline_started.emit()
handle_next_event()
## Preloader function, prepares a timeline and returns an object to hold for later
## [param timeline_resource] can be either a path (string) or a loaded timeline (resource)
func preload_timeline(timeline_resource:Variant) -> Variant:
# I think ideally this should be on a new thread, will test
if typeof(timeline_resource) in [TYPE_STRING, TYPE_STRING_NAME]:
if "://" in timeline_resource:
timeline_resource = load(timeline_resource)
else:
timeline_resource = DialogicResourceUtil.get_timeline_resource(timeline_resource)
if timeline_resource == null:
printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors")
return null
(timeline_resource as DialogicTimeline).process()
return timeline_resource
## Clears and stops the current timeline.
## If [param skip_ending] is `true`, the dialog_ending_timeline is not getting played
func end_timeline(skip_ending := false) -> void:
if not skip_ending and dialog_ending_timeline and current_timeline != dialog_ending_timeline:
start(dialog_ending_timeline)
return
await clear(ClearFlags.TIMELINE_INFO_ONLY)
if Styles.has_active_layout_node() and Styles.get_layout_node().is_inside_tree():
match ProjectSettings.get_setting("dialogic/layout/end_behaviour", 0):
0:
Styles.get_layout_node().get_parent().remove_child(Styles.get_layout_node())
Styles.get_layout_node().queue_free()
1:
Styles.get_layout_node().hide()
timeline_ended.emit()
## Method to check if timeline exists.
## @timeline can be either a loaded timeline resource or a path to a timeline file.
func timeline_exists(timeline:Variant) -> bool:
if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]:
if "://" in timeline and ResourceLoader.exists(timeline):
return load(timeline) is DialogicTimeline
else:
return DialogicResourceUtil.timeline_resource_exists(timeline)
return timeline is DialogicTimeline
## Handles the next event.
func handle_next_event(_ignore_argument: Variant = "") -> void:
handle_event(current_event_idx+1)
## Handles the event at the given index [param event_index].
## You can call this manually, but if another event is still executing, it might have unexpected results.
func handle_event(event_index:int) -> void:
if not current_timeline:
return
_cleanup_previous_event()
if paused:
await dialogic_resumed
if event_index >= len(current_timeline_events):
end_timeline()
return
# TODO: Check if necessary. This should be impossible.
#actually process the event now, since we didnt earlier at runtime
#this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready
if current_timeline_events[event_index].event_node_ready == false:
current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index].event_node_as_text)
current_event_idx = event_index
if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event):
current_timeline_events[event_index].event_finished.connect(handle_next_event)
set_meta("previous_event", current_timeline_events[event_index])
current_timeline_events[event_index].execute(self)
event_handled.emit(current_timeline_events[event_index])
## Resets Dialogic's state fully or partially.
## By using the clear flags from the [member ClearFlags] enum you can specify
## what info should be kept.
## For example, at timeline end usually it doesn't clear node or subsystem info.
func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void:
_cleanup_previous_event()
if not clear_flags & ClearFlags.TIMELINE_INFO_ONLY:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem)._clear_state(clear_flags)
current_event_idx = -1
current_timeline_events = []
current_state = States.IDLE
# Resetting variables
var previous_timeline := current_timeline
current_timeline = null
if previous_timeline:
await previous_timeline.clean()
## Cleanup after previous event (if any).
func _cleanup_previous_event():
if has_meta("previous_event") and get_meta("previous_event") is DialogicEvent:
var event := get_meta("previous_event") as DialogicEvent
if event.event_finished.is_connected(handle_next_event):
event.event_finished.disconnect(handle_next_event)
event._clear_state()
remove_meta("previous_event")
#endregion
#region SAVING & LOADING
################################################################################
## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state.
## The [subsystem Save] subsystem might be more useful for you.
## However, this can be used to integrate the info into your own save system.
func get_full_state() -> DialogicSaveState:
var state := DialogicSaveState.new()
if current_timeline:
state.event_index = current_event_idx
state.timeline = current_timeline.resource_path
else:
state.event_index = -1
state.timeline = ""
for subsystem in get_children():
var sub_state := (subsystem as DialogicSubsystem).get_state()
if sub_state:
state.subsystems[subsystem.name] = sub_state
return state
## This method tries to load the state from the given [param state_info].
## Will automatically start a timeline and add a layout if a timeline was running when
## the dictionary was retrieved with [method get_full_state].
func load_full_state(state:DialogicSaveState) -> void:
await clear()
if state == null:
printerr("[Dialogic] Attempted to load state, but given state was [null].")
return
for subsystem in get_children():
if subsystem.name in state.subsystems:
subsystem.unpack_state(state.subsystems[subsystem.name])
### The Style subsystem needs to run first for others to load correctly.
var scene: Node = null
if has_subsystem("Styles"):
get_subsystem("Styles").load_state()
scene = self.Styles.get_layout_node()
var load_subsystems := func() -> void:
for subsystem in get_children():
if subsystem.name == "Styles":
continue
(subsystem as DialogicSubsystem).load_state()
if null != scene and not scene.is_node_ready():
scene.ready.connect(load_subsystems)
else:
await get_tree().process_frame
load_subsystems.call()
#
if state.timeline:
start_timeline(state.timeline, state.event_index)
else:
end_timeline.call_deferred(true)
#endregion
#region SUB-SYSTEMS
################################################################################
func _collect_subsystems() -> void:
var subsystem_nodes := [] as Array[DialogicSubsystem]
for indexer in DialogicUtil.get_indexers():
for subsystem in indexer._get_subsystems():
var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script))
subsystem_nodes.push_back(subsystem_node)
for subsystem in subsystem_nodes:
subsystem._post_install()
## Returns `true` if a subystem with the given [param subsystem_name] exists.
func has_subsystem(subsystem_name:String) -> bool:
return has_node(subsystem_name)
## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist.
func get_subsystem(subsystem_name:String) -> DialogicSubsystem:
return get_node(subsystem_name)
## Adds a subsystem node with the given [param subsystem_name] and [param script_path].
func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem:
var existing_subsystem_node = get_node_or_null(subsystem_name)
# If two Subsystem have the same name, we override the existing one with the new one
if is_instance_valid(existing_subsystem_node):
existing_subsystem_node.set_script(load(script_path))
existing_subsystem_node.dialogic = self
existing_subsystem_node._ready()
return existing_subsystem_node
var node: Node = Node.new()
node.name = subsystem_name
node.set_script(load(script_path))
node = node as DialogicSubsystem
node.dialogic = self
add_child(node)
return node
#endregion
#region HELPERS
################################################################################
func print_debug_moment() -> void:
if not current_timeline:
return
printerr("\t> On line {line} of {timeline_identifier} ({timeline_path}) at event {event_idx} ({event_name} Event).".format(
{
"event_idx": current_event_idx+1,
"event_name":current_timeline_events[current_event_idx].event_name,
"timeline_identifier": current_timeline.get_identifier(),
"timeline_path":current_timeline.resource_path,
"line":current_timeline.get_text_line_from_index(current_event_idx+1)
}))
print("\n")
func instantiate_debug_overlay() -> void:
var overlay: Node = load("res://addons/dialogic/Editor/TimelineEditor/dialogic_debug_overlay.tscn").instantiate()
get_parent().add_child.call_deferred(overlay)
#endregion