1030 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			GDScript
		
	
			
		
		
	
	
			1030 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			GDScript
		
	
extends Node
 | 
						|
 | 
						|
 | 
						|
signal passed_title(title)
 | 
						|
signal got_dialogue(line)
 | 
						|
signal mutated(mutation)
 | 
						|
signal dialogue_ended(resource)
 | 
						|
signal bridge_get_next_dialogue_line_completed(line)
 | 
						|
 | 
						|
 | 
						|
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
 | 
						|
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
 | 
						|
const DialogueLine = preload("res://addons/dialogue_manager/dialogue_line.gd")
 | 
						|
const DialogueResponse = preload("res://addons/dialogue_manager/dialogue_response.gd")
 | 
						|
 | 
						|
const SUPPORTED_ARRAY_METHODS = [
 | 
						|
	"assign",
 | 
						|
	"append",
 | 
						|
	"append_array",
 | 
						|
	"back",
 | 
						|
	"count",
 | 
						|
	"clear",
 | 
						|
	"erase",
 | 
						|
	"has",
 | 
						|
	"insert",
 | 
						|
	"is_empty",
 | 
						|
	"max",
 | 
						|
	"min",
 | 
						|
	"pick_random",
 | 
						|
	"pop_at",
 | 
						|
	"pop_back",
 | 
						|
	"pop_front",
 | 
						|
	"push_back",
 | 
						|
	"push_front",
 | 
						|
	"remove_at",
 | 
						|
	"reverse",
 | 
						|
	"shuffle",
 | 
						|
	"size",
 | 
						|
	"sort"
 | 
						|
]
 | 
						|
const SUPPORTED_DICTIONARY_METHODS = ["has", "has_all", "get", "keys", "values", "size"]
 | 
						|
 | 
						|
 | 
						|
enum MutationBehaviour {
 | 
						|
	Wait,
 | 
						|
	DoNotWait,
 | 
						|
	Skip
 | 
						|
}
 | 
						|
 | 
						|
enum TranslationSource {
 | 
						|
	None,
 | 
						|
	Guess,
 | 
						|
	CSV,
 | 
						|
	PO
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
# The list of globals that dialogue can query
 | 
						|
var game_states: Array = []
 | 
						|
 | 
						|
# Allow dialogue to call singletons
 | 
						|
var include_singletons: bool = true
 | 
						|
 | 
						|
# Manage translation behaviour
 | 
						|
var translation_source: TranslationSource = TranslationSource.Guess
 | 
						|
 | 
						|
var _node_properties: Array = []
 | 
						|
 | 
						|
 | 
						|
func _ready() -> void:
 | 
						|
	# Cache the known Node2D properties
 | 
						|
	_node_properties = ["Script Variables"]
 | 
						|
	var temp_node: Node2D = Node2D.new()
 | 
						|
	for property in temp_node.get_property_list():
 | 
						|
		_node_properties.append(property.name)
 | 
						|
	temp_node.free()
 | 
						|
 | 
						|
	# Add any autoloads to a generic state so we can refer to them by name
 | 
						|
	var autoloads: Dictionary = {}
 | 
						|
	for child in get_tree().root.get_children():
 | 
						|
		# Ignore the dialogue manager
 | 
						|
		if child.name == StringName("DialogueManager"): continue
 | 
						|
		# Ignore the current main scene
 | 
						|
		if get_tree().current_scene and child.name == get_tree().current_scene.name: continue
 | 
						|
		# Add the node to our known autoloads
 | 
						|
		autoloads[child.name] = child
 | 
						|
	game_states = [autoloads]
 | 
						|
 | 
						|
	# Add any other state shortcuts from settings
 | 
						|
	for node_name in DialogueSettings.get_setting("states", []):
 | 
						|
		var state: Node = get_node_or_null("/root/" + node_name)
 | 
						|
		if state:
 | 
						|
			game_states.append(state)
 | 
						|
 | 
						|
	# Make the dialogue manager available as a singleton
 | 
						|
	Engine.register_singleton("DialogueManager", self)
 | 
						|
 | 
						|
 | 
						|
## Step through lines and run any mutations until we either hit some dialogue or the end of the conversation
 | 
						|
func get_next_dialogue_line(resource: DialogueResource, key: String = "0", extra_game_states: Array = [], mutation_behaviour: MutationBehaviour = MutationBehaviour.Wait) -> DialogueLine:
 | 
						|
	# You have to provide a valid dialogue resource
 | 
						|
	assert(resource != null, DialogueConstants.translate("runtime.no_resource"))
 | 
						|
	assert(resource.lines.size() > 0, DialogueConstants.translate("runtime.no_content").format({ file_path = resource.resource_path }))
 | 
						|
 | 
						|
	var dialogue: DialogueLine = await get_line(resource, key, extra_game_states)
 | 
						|
 | 
						|
	# If our dialogue is nothing then we hit the end
 | 
						|
	if not is_valid(dialogue):
 | 
						|
		dialogue_ended.emit(resource)
 | 
						|
		return null
 | 
						|
 | 
						|
	# Run the mutation if it is one
 | 
						|
	if dialogue.type == DialogueConstants.TYPE_MUTATION:
 | 
						|
		var actual_next_id: String = dialogue.next_id.split(",")[0]
 | 
						|
		match mutation_behaviour:
 | 
						|
			MutationBehaviour.Wait:
 | 
						|
				await mutate(dialogue.mutation, extra_game_states)
 | 
						|
			MutationBehaviour.DoNotWait:
 | 
						|
				mutate(dialogue.mutation, extra_game_states)
 | 
						|
			MutationBehaviour.Skip:
 | 
						|
				pass
 | 
						|
		if actual_next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]:
 | 
						|
			# End the conversation
 | 
						|
			dialogue_ended.emit(resource)
 | 
						|
			return null
 | 
						|
		else:
 | 
						|
			return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour)
 | 
						|
	else:
 | 
						|
		got_dialogue.emit(dialogue)
 | 
						|
		return dialogue
 | 
						|
 | 
						|
 | 
						|
## Replace any variables, etc in the dialogue with their state values
 | 
						|
func get_resolved_text(text: String, replacements: Array, extra_game_states: Array = []) -> String:
 | 
						|
	# Resolve variables
 | 
						|
	for replacement in replacements:
 | 
						|
		var value = await resolve(replacement.expression.duplicate(true), extra_game_states)
 | 
						|
		text = text.replace(replacement.value_in_text, str(value))
 | 
						|
 | 
						|
	# Resolve random groups
 | 
						|
	var random_regex: RegEx = RegEx.new()
 | 
						|
	random_regex.compile("\\[\\[(?<options>.*?)\\]\\]")
 | 
						|
	for found in random_regex.search_all(text):
 | 
						|
		var options = found.get_string("options").split("|")
 | 
						|
		text = text.replace("[[%s]]" % found.get_string("options"), options[randi_range(0, options.size() - 1)])
 | 
						|
 | 
						|
	return text
 | 
						|
 | 
						|
 | 
						|
## Generate a dialogue resource on the fly from some text
 | 
						|
func create_resource_from_text(text: String) -> Resource:
 | 
						|
	var parser: DialogueManagerParser = DialogueManagerParser.new()
 | 
						|
	parser.parse(text, "")
 | 
						|
	var results: DialogueManagerParseResult = parser.get_data()
 | 
						|
	var errors: Array[Dictionary] = parser.get_errors()
 | 
						|
	parser.free()
 | 
						|
 | 
						|
	if errors.size() > 0:
 | 
						|
		printerr(DialogueConstants.translate("runtime.errors").format({ count = errors.size() }))
 | 
						|
		for error in errors:
 | 
						|
			printerr(DialogueConstants.translate("runtime.error_detail").format({
 | 
						|
				line = error.line_number + 1,
 | 
						|
				message = DialogueConstants.get_error_message(error.error)
 | 
						|
			}))
 | 
						|
		assert(false, DialogueConstants.translate("runtime.errors_see_details").format({ count = errors.size() }))
 | 
						|
 | 
						|
	var resource: DialogueResource = DialogueResource.new()
 | 
						|
	resource.titles = results.titles
 | 
						|
	resource.character_names = results.character_names
 | 
						|
	resource.lines = results.lines
 | 
						|
 | 
						|
	return resource
 | 
						|
 | 
						|
 | 
						|
## Show the example balloon
 | 
						|
func show_example_dialogue_balloon(resource: DialogueResource, title: String = "0", extra_game_states: Array = []) -> void:
 | 
						|
	var ExampleBalloonScene = load("res://addons/dialogue_manager/example_balloon/example_balloon.tscn")
 | 
						|
	var SmallExampleBalloonScene = load("res://addons/dialogue_manager/example_balloon/small_example_balloon.tscn")
 | 
						|
 | 
						|
	var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400
 | 
						|
	var balloon: Node = (SmallExampleBalloonScene if is_small_window else ExampleBalloonScene).instantiate()
 | 
						|
	get_tree().current_scene.add_child(balloon)
 | 
						|
	balloon.start(resource, title, extra_game_states)
 | 
						|
 | 
						|
 | 
						|
### Dotnet bridge
 | 
						|
 | 
						|
 | 
						|
func _bridge_get_next_dialogue_line(resource: DialogueResource, key: String, extra_game_states: Array = []) -> void:
 | 
						|
	# dotnet needs at least one await tick of the signal gets called too quickly
 | 
						|
	await get_tree().process_frame
 | 
						|
 | 
						|
	var line = await get_next_dialogue_line(resource, key, extra_game_states)
 | 
						|
	bridge_get_next_dialogue_line_completed.emit(line)
 | 
						|
 | 
						|
 | 
						|
### Helpers
 | 
						|
 | 
						|
 | 
						|
# Get a line by its ID
 | 
						|
func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine:
 | 
						|
	key = key.strip_edges()
 | 
						|
 | 
						|
	# See if we were given a stack instead of just the one key
 | 
						|
	var stack: Array = key.split("|")
 | 
						|
	key = stack.pop_front()
 | 
						|
	var id_trail: String = "" if stack.size() == 0 else "|" + "|".join(stack)
 | 
						|
 | 
						|
	# See if we just ended the conversation
 | 
						|
	if key in [DialogueConstants.ID_END, DialogueConstants.ID_NULL, null]:
 | 
						|
		if stack.size() > 0:
 | 
						|
			return await get_line(resource, "|".join(stack), extra_game_states)
 | 
						|
		else:
 | 
						|
			return null
 | 
						|
	elif key == DialogueConstants.ID_END_CONVERSATION:
 | 
						|
		return null
 | 
						|
 | 
						|
	# See if it is a title
 | 
						|
	if key.begins_with("~ "):
 | 
						|
		key = key.substr(2)
 | 
						|
	if resource.titles.has(key):
 | 
						|
		key = resource.titles.get(key)
 | 
						|
 | 
						|
	if key in resource.titles.values():
 | 
						|
		passed_title.emit(resource.titles.find_key(key))
 | 
						|
 | 
						|
	# Key not found, just use the first title
 | 
						|
	if not resource.lines.has(key):
 | 
						|
		key = resource.first_title
 | 
						|
 | 
						|
	var data: Dictionary = resource.lines.get(key)
 | 
						|
 | 
						|
	# Check for weighted random lines
 | 
						|
	if data.has("siblings"):
 | 
						|
		var result = randi() % data.siblings.reduce(func(total, sibling): return total + sibling.weight, 0)
 | 
						|
		var cummulative_weight = 0
 | 
						|
		for sibling in data.siblings:
 | 
						|
			if result < cummulative_weight + sibling.weight:
 | 
						|
				data = resource.lines.get(sibling.id)
 | 
						|
				break
 | 
						|
			else:
 | 
						|
				cummulative_weight += sibling.weight
 | 
						|
 | 
						|
	# Check condtiions
 | 
						|
	elif data.type == DialogueConstants.TYPE_CONDITION:
 | 
						|
		# "else" will have no actual condition
 | 
						|
		if await check_condition(data, extra_game_states):
 | 
						|
			return await get_line(resource, data.next_id + id_trail, extra_game_states)
 | 
						|
		else:
 | 
						|
			return await get_line(resource, data.next_conditional_id + id_trail, extra_game_states)
 | 
						|
 | 
						|
	# Evaluate jumps
 | 
						|
	elif data.type == DialogueConstants.TYPE_GOTO:
 | 
						|
		if data.is_snippet:
 | 
						|
			id_trail = "|" + data.next_id_after + id_trail
 | 
						|
		return await get_line(resource, data.next_id + id_trail, extra_game_states)
 | 
						|
 | 
						|
	# Set up a line object
 | 
						|
	var line: DialogueLine = await create_dialogue_line(data, extra_game_states)
 | 
						|
 | 
						|
	# If the jump point somehow has no content then just end
 | 
						|
	if not line: return null
 | 
						|
 | 
						|
	# If we are the first of a list of responses then get the other ones
 | 
						|
	if data.type == DialogueConstants.TYPE_RESPONSE:
 | 
						|
		line.responses = await get_responses(data.responses, resource, id_trail, extra_game_states)
 | 
						|
		return line
 | 
						|
 | 
						|
	# Inject the next node's responses if they have any
 | 
						|
	if resource.lines.has(line.next_id):
 | 
						|
		var next_line: Dictionary = resource.lines.get(line.next_id)
 | 
						|
		if next_line != null and next_line.type == DialogueConstants.TYPE_RESPONSE:
 | 
						|
			line.responses = await get_responses(next_line.responses, resource, id_trail, extra_game_states)
 | 
						|
 | 
						|
	line.next_id += id_trail
 | 
						|
	return line
 | 
						|
 | 
						|
 | 
						|
# Translate a string
 | 
						|
func translate(data: Dictionary) -> String:
 | 
						|
	if translation_source == TranslationSource.None:
 | 
						|
		return data.text
 | 
						|
 | 
						|
	if data.translation_key == "" or data.translation_key == data.text:
 | 
						|
		return tr(data.text)
 | 
						|
	else:
 | 
						|
		# Line IDs work slightly differently depending on whether the translation came from a
 | 
						|
		# CSV or a PO file. CSVs use the line ID (or the line itself) as the translatable string
 | 
						|
		# whereas POs use the ID as context and the line itself as the translatable string.
 | 
						|
		match translation_source:
 | 
						|
			TranslationSource.PO:
 | 
						|
				return tr(data.text, StringName(data.translation_key))
 | 
						|
 | 
						|
			TranslationSource.CSV:
 | 
						|
				return tr(data.translation_key)
 | 
						|
 | 
						|
			TranslationSource.Guess:
 | 
						|
				var translation_files: Array = ProjectSettings.get_setting("internationalization/locale/translations")
 | 
						|
				if translation_files.filter(func(f: String): return f.get_extension() == "po").size() > 0:
 | 
						|
					# Assume PO
 | 
						|
					return tr(data.text, StringName(data.translation_key))
 | 
						|
				else:
 | 
						|
					# Assume CSV
 | 
						|
					return tr(data.translation_key)
 | 
						|
 | 
						|
	return tr(data.translation_key)
 | 
						|
 | 
						|
 | 
						|
# Create a line of dialogue
 | 
						|
func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> DialogueLine:
 | 
						|
	match data.type:
 | 
						|
		DialogueConstants.TYPE_DIALOGUE:
 | 
						|
			# Our bbcodes need to be process after text has been resolved so that the markers are at the correct index
 | 
						|
			var text: String = await get_resolved_text(translate(data), data.text_replacements, extra_game_states)
 | 
						|
			var markers: Dictionary = DialogueManagerParser.extract_markers_from_string(text)
 | 
						|
 | 
						|
			return DialogueLine.new({
 | 
						|
				type = DialogueConstants.TYPE_DIALOGUE,
 | 
						|
				next_id = data.next_id,
 | 
						|
				character = await get_resolved_text(data.character, data.character_replacements, extra_game_states),
 | 
						|
				character_replacements = data.character_replacements,
 | 
						|
				text = markers.text,
 | 
						|
				text_replacements = data.text_replacements,
 | 
						|
				translation_key = data.translation_key,
 | 
						|
				pauses = markers.pauses,
 | 
						|
				speeds = markers.speeds,
 | 
						|
				inline_mutations = markers.mutations,
 | 
						|
				time = markers.time,
 | 
						|
				extra_game_states = extra_game_states
 | 
						|
			})
 | 
						|
 | 
						|
		DialogueConstants.TYPE_RESPONSE:
 | 
						|
			return DialogueLine.new({
 | 
						|
				type = DialogueConstants.TYPE_RESPONSE,
 | 
						|
				next_id = data.next_id,
 | 
						|
				extra_game_states = extra_game_states
 | 
						|
			})
 | 
						|
 | 
						|
		DialogueConstants.TYPE_MUTATION:
 | 
						|
			return DialogueLine.new({
 | 
						|
				type = DialogueConstants.TYPE_MUTATION,
 | 
						|
				next_id = data.next_id,
 | 
						|
				mutation = data.mutation,
 | 
						|
				extra_game_states = extra_game_states
 | 
						|
			})
 | 
						|
 | 
						|
	return null
 | 
						|
 | 
						|
 | 
						|
# Create a response
 | 
						|
func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResponse:
 | 
						|
	return DialogueResponse.new({
 | 
						|
		type = DialogueConstants.TYPE_RESPONSE,
 | 
						|
		next_id = data.next_id,
 | 
						|
		is_allowed = await check_condition(data, extra_game_states),
 | 
						|
		text = await get_resolved_text(translate(data), data.text_replacements, extra_game_states),
 | 
						|
		text_replacements = data.text_replacements,
 | 
						|
		translation_key = data.translation_key
 | 
						|
	})
 | 
						|
 | 
						|
 | 
						|
# Get the current game states
 | 
						|
func get_game_states(extra_game_states: Array) -> Array:
 | 
						|
	var current_scene: Node = get_tree().current_scene
 | 
						|
	var unique_states: Array = []
 | 
						|
	for state in extra_game_states + [current_scene] + game_states:
 | 
						|
		if state != null and not unique_states.has(state):
 | 
						|
			unique_states.append(state)
 | 
						|
	return unique_states
 | 
						|
 | 
						|
 | 
						|
# Check if a condition is met
 | 
						|
func check_condition(data: Dictionary, extra_game_states: Array) -> bool:
 | 
						|
	if data.get("condition", null) == null: return true
 | 
						|
	if data.condition.size() == 0: return true
 | 
						|
 | 
						|
	return await resolve(data.condition.expression.duplicate(true), extra_game_states)
 | 
						|
 | 
						|
 | 
						|
# Make a change to game state or run a method
 | 
						|
func mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void:
 | 
						|
	var expression: Array[Dictionary] = mutation.expression
 | 
						|
 | 
						|
	# Handle built in mutations
 | 
						|
	if expression[0].type == DialogueConstants.TOKEN_FUNCTION and expression[0].function in ["wait", "emit", "debug"]:
 | 
						|
		var args: Array = await resolve_each(expression[0].value, extra_game_states)
 | 
						|
		match expression[0].function:
 | 
						|
			"wait":
 | 
						|
				mutated.emit(mutation)
 | 
						|
				await get_tree().create_timer(float(args[0])).timeout
 | 
						|
				return
 | 
						|
 | 
						|
			"emit":
 | 
						|
				for state in get_game_states(extra_game_states):
 | 
						|
					if typeof(state) == TYPE_DICTIONARY:
 | 
						|
						continue
 | 
						|
					elif state.has_signal(args[0]):
 | 
						|
						match args.size():
 | 
						|
							1:
 | 
						|
								state.emit_signal(args[0])
 | 
						|
							2:
 | 
						|
								state.emit_signal(args[0], args[1])
 | 
						|
							3:
 | 
						|
								state.emit_signal(args[0], args[1], args[2])
 | 
						|
							4:
 | 
						|
								state.emit_signal(args[0], args[1], args[2], args[3])
 | 
						|
							5:
 | 
						|
								state.emit_signal(args[0], args[1], args[2], args[3], args[4])
 | 
						|
							6:
 | 
						|
								state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5])
 | 
						|
							7:
 | 
						|
								state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
 | 
						|
							8:
 | 
						|
								state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7])
 | 
						|
						return
 | 
						|
 | 
						|
				# The signal hasn't been found anywhere
 | 
						|
				assert(false, DialogueConstants.translate("runtime.signal_not_found").format({ signal_name = args[0], states = str(get_game_states(extra_game_states)) }))
 | 
						|
 | 
						|
			"debug":
 | 
						|
				prints("Debug:", args)
 | 
						|
				await get_tree().process_frame
 | 
						|
 | 
						|
	# Or pass through to the resolver
 | 
						|
	else:
 | 
						|
		if not mutation_contains_assignment(mutation.expression) and not is_inline_mutation:
 | 
						|
			mutated.emit(mutation)
 | 
						|
 | 
						|
		await resolve(mutation.expression.duplicate(true), extra_game_states)
 | 
						|
		return
 | 
						|
 | 
						|
	# Wait one frame to give the dialogue handler a chance to yield
 | 
						|
	await get_tree().process_frame
 | 
						|
 | 
						|
 | 
						|
func mutation_contains_assignment(mutation: Array) -> bool:
 | 
						|
	for token in mutation:
 | 
						|
		if token.type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
			return true
 | 
						|
	return false
 | 
						|
 | 
						|
 | 
						|
func resolve_each(array: Array, extra_game_states: Array) -> Array:
 | 
						|
	var results: Array = []
 | 
						|
	for item in array:
 | 
						|
		results.append(await resolve(item.duplicate(true), extra_game_states))
 | 
						|
	return results
 | 
						|
 | 
						|
 | 
						|
# Replace an array of line IDs with their response prompts
 | 
						|
func get_responses(ids: Array, resource: DialogueResource, id_trail: String, extra_game_states: Array) -> Array[DialogueResponse]:
 | 
						|
	var responses: Array[DialogueResponse] = []
 | 
						|
	for id in ids:
 | 
						|
		var data: Dictionary = resource.lines.get(id)
 | 
						|
		if DialogueSettings.get_setting("include_all_responses", false) or await check_condition(data, extra_game_states):
 | 
						|
			var response: DialogueResponse = await create_response(data, extra_game_states)
 | 
						|
			response.next_id += id_trail
 | 
						|
			responses.append(response)
 | 
						|
 | 
						|
	return responses
 | 
						|
 | 
						|
 | 
						|
# Get a value on the current scene or game state
 | 
						|
func get_state_value(property: String, extra_game_states: Array):
 | 
						|
	var expression = Expression.new()
 | 
						|
	if expression.parse(property) != OK:
 | 
						|
		assert(false, DialogueConstants.translate("runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() }))
 | 
						|
 | 
						|
	for state in get_game_states(extra_game_states):
 | 
						|
		if typeof(state) == TYPE_DICTIONARY:
 | 
						|
			if state.has(property):
 | 
						|
				return state.get(property)
 | 
						|
		else:
 | 
						|
			var result = expression.execute([], state, false)
 | 
						|
			if not expression.has_execute_failed():
 | 
						|
				return result
 | 
						|
 | 
						|
	if include_singletons and Engine.has_singleton(property):
 | 
						|
		return Engine.get_singleton(property)
 | 
						|
 | 
						|
	assert(false, DialogueConstants.translate("runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) }))
 | 
						|
 | 
						|
 | 
						|
# Set a value on the current scene or game state
 | 
						|
func set_state_value(property: String, value, extra_game_states: Array) -> void:
 | 
						|
	for state in get_game_states(extra_game_states):
 | 
						|
		if typeof(state) == TYPE_DICTIONARY:
 | 
						|
			if state.has(property):
 | 
						|
				state[property] = value
 | 
						|
				return
 | 
						|
		elif thing_has_property(state, property):
 | 
						|
			state.set(property, value)
 | 
						|
			return
 | 
						|
 | 
						|
	assert(false, DialogueConstants.translate("runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) }))
 | 
						|
 | 
						|
 | 
						|
# Collapse any expressions
 | 
						|
func resolve(tokens: Array, extra_game_states: Array):
 | 
						|
	# Handle groups first
 | 
						|
	for token in tokens:
 | 
						|
		if token.type == DialogueConstants.TOKEN_GROUP:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = await resolve(token.value, extra_game_states)
 | 
						|
 | 
						|
	# Then variables/methods
 | 
						|
	var i: int = 0
 | 
						|
	var limit: int = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
 | 
						|
		if token.type == DialogueConstants.TOKEN_FUNCTION:
 | 
						|
			var function_name: String = token.function
 | 
						|
			var args = await resolve_each(token.value, extra_game_states)
 | 
						|
			match function_name:
 | 
						|
				"str":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = str(args[0])
 | 
						|
				"Vector2":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector2(args[0], args[1])
 | 
						|
				"Vector2i":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector2i(args[0], args[1])
 | 
						|
				"Vector3":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector3(args[0], args[1], args[2])
 | 
						|
				"Vector3i":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector3i(args[0], args[1], args[2])
 | 
						|
				"Vector4":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector4(args[0], args[1], args[2], args[3])
 | 
						|
				"Vector4i":
 | 
						|
					token["type"] = "value"
 | 
						|
					token["value"] = Vector4i(args[0], args[1], args[2], args[3])
 | 
						|
				_:
 | 
						|
					if tokens[i - 1].type == DialogueConstants.TOKEN_DOT:
 | 
						|
						# If we are calling a deeper function then we need to collapse the
 | 
						|
						# value into the thing we are calling the function on
 | 
						|
						var caller: Dictionary = tokens[i - 2]
 | 
						|
						if typeof(caller.value) == TYPE_DICTIONARY:
 | 
						|
							caller["type"] = "value"
 | 
						|
							caller["value"] = resolve_dictionary_method(caller.value, function_name, args)
 | 
						|
							tokens.remove_at(i)
 | 
						|
							tokens.remove_at(i-1)
 | 
						|
							i -= 2
 | 
						|
						elif typeof(caller.value) == TYPE_ARRAY:
 | 
						|
							caller["type"] = "value"
 | 
						|
							caller["value"] = resolve_array_method(caller.value, function_name, args)
 | 
						|
							tokens.remove_at(i)
 | 
						|
							tokens.remove_at(i-1)
 | 
						|
							i -= 2
 | 
						|
						elif thing_has_method(caller.value, function_name, args):
 | 
						|
							caller["type"] = "value"
 | 
						|
							caller["value"] = await caller.value.callv(function_name, args)
 | 
						|
							tokens.remove_at(i)
 | 
						|
							tokens.remove_at(i-1)
 | 
						|
							i -= 2
 | 
						|
						else:
 | 
						|
							assert(false, DialogueConstants.translate("runtime.method_not_callable").format({ method = function_name, object = str(caller) }))
 | 
						|
					else:
 | 
						|
						var found: bool = false
 | 
						|
						for state in get_game_states(extra_game_states):
 | 
						|
							if typeof(state) == TYPE_DICTIONARY and function_name in SUPPORTED_DICTIONARY_METHODS:
 | 
						|
								token["type"] = "value"
 | 
						|
								token["value"] = resolve_dictionary_method(state, function_name, args)
 | 
						|
								found = true
 | 
						|
							elif typeof(state) == TYPE_ARRAY and function_name in SUPPORTED_ARRAY_METHODS:
 | 
						|
								token["type"] = "value"
 | 
						|
								token["value"] = resolve_array_method(state, function_name, args)
 | 
						|
								found = true
 | 
						|
							elif thing_has_method(state, function_name, args):
 | 
						|
								token["type"] = "value"
 | 
						|
								token["value"] = await state.callv(function_name, args)
 | 
						|
								found = true
 | 
						|
 | 
						|
							if found:
 | 
						|
								break
 | 
						|
 | 
						|
						assert(found, DialogueConstants.translate("runtime.method_not_found").format({
 | 
						|
							method = args[0] if function_name in ["call", "call_deferred"] else function_name,
 | 
						|
							states = str(get_game_states(extra_game_states))
 | 
						|
						}))
 | 
						|
 | 
						|
		elif token.type == DialogueConstants.TOKEN_DICTIONARY_REFERENCE:
 | 
						|
			var value
 | 
						|
			if i > 0 and tokens[i - 1].type == DialogueConstants.TOKEN_DOT:
 | 
						|
				# If we are deep referencing then we need to get the parent object.
 | 
						|
				# `parent.value` is the actual object and `token.variable` is the name of
 | 
						|
				# the property within it.
 | 
						|
				value = tokens[i - 2].value[token.variable]
 | 
						|
				# Clean up the previous tokens
 | 
						|
				token.erase("variable")
 | 
						|
				tokens.remove_at(i - 1)
 | 
						|
				tokens.remove_at(i - 2)
 | 
						|
				i -= 2
 | 
						|
			else:
 | 
						|
				# Otherwise we can just get this variable as a normal state reference
 | 
						|
				value = get_state_value(token.variable, extra_game_states)
 | 
						|
 | 
						|
			var index = await resolve(token.value, extra_game_states)
 | 
						|
			if typeof(value) == TYPE_DICTIONARY:
 | 
						|
				if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
					# If the next token is an assignment then we need to leave this as a reference
 | 
						|
					# so that it can be resolved once everything ahead of it has been resolved
 | 
						|
					token["type"] = "dictionary"
 | 
						|
					token["value"] = value
 | 
						|
					token["key"] = index
 | 
						|
				else:
 | 
						|
					if value.has(index):
 | 
						|
						token["type"] = "value"
 | 
						|
						token["value"] = value[index]
 | 
						|
					else:
 | 
						|
						assert(false, DialogueConstants.translate("runtime.key_not_found").format({ key = str(index), dictionary = token.variable }))
 | 
						|
			elif typeof(value) == TYPE_ARRAY:
 | 
						|
				if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
					# If the next token is an assignment then we need to leave this as a reference
 | 
						|
					# so that it can be resolved once everything ahead of it has been resolved
 | 
						|
					token["type"] = "array"
 | 
						|
					token["value"] = value
 | 
						|
					token["key"] = index
 | 
						|
				else:
 | 
						|
					if index >= 0 and index < value.size():
 | 
						|
						token["type"] = "value"
 | 
						|
						token["value"] = value[index]
 | 
						|
					else:
 | 
						|
						assert(false, DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = index, array = token.variable }))
 | 
						|
 | 
						|
		elif token.type == DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE:
 | 
						|
			var dictionary: Dictionary = tokens[i - 1]
 | 
						|
			var index = await resolve(token.value, extra_game_states)
 | 
						|
			var value = dictionary.value
 | 
						|
			if typeof(value) == TYPE_DICTIONARY:
 | 
						|
				if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
					# If the next token is an assignment then we need to leave this as a reference
 | 
						|
					# so that it can be resolved once everything ahead of it has been resolved
 | 
						|
					dictionary["type"] = "dictionary"
 | 
						|
					dictionary["key"] = index
 | 
						|
					dictionary["value"] = value
 | 
						|
					tokens.remove_at(i)
 | 
						|
					i -= 1
 | 
						|
				else:
 | 
						|
					if dictionary.value.has(index):
 | 
						|
						dictionary["value"] = value.get(index)
 | 
						|
						tokens.remove_at(i)
 | 
						|
						i -= 1
 | 
						|
					else:
 | 
						|
						assert(false, DialogueConstants.translate("runtime.key_not_found").format({ key = str(index), dictionary = value }))
 | 
						|
			elif typeof(value) == TYPE_ARRAY:
 | 
						|
				if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
					# If the next token is an assignment then we need to leave this as a reference
 | 
						|
					# so that it can be resolved once everything ahead of it has been resolved
 | 
						|
					dictionary["type"] = "array"
 | 
						|
					dictionary["value"] = value
 | 
						|
					dictionary["key"] = index
 | 
						|
					tokens.remove_at(i)
 | 
						|
					i -= 1
 | 
						|
				else:
 | 
						|
					if index >= 0 and index < value.size():
 | 
						|
						dictionary["value"] = value[index]
 | 
						|
						tokens.remove_at(i)
 | 
						|
						i -= 1
 | 
						|
					else:
 | 
						|
						assert(false, DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = index, array = value }))
 | 
						|
 | 
						|
		elif token.type == DialogueConstants.TOKEN_ARRAY:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = await resolve_each(token.value, extra_game_states)
 | 
						|
 | 
						|
		elif token.type == DialogueConstants.TOKEN_DICTIONARY:
 | 
						|
			token["type"] = "value"
 | 
						|
			var dictionary = {}
 | 
						|
			for key in token.value.keys():
 | 
						|
				var resolved_key = await resolve([key], extra_game_states)
 | 
						|
				var resolved_value = await resolve([token.value.get(key)], extra_game_states)
 | 
						|
				dictionary[resolved_key] = resolved_value
 | 
						|
			token["value"] = dictionary
 | 
						|
 | 
						|
		elif token.type == DialogueConstants.TOKEN_VARIABLE or token.type == DialogueConstants.TOKEN_NUMBER:
 | 
						|
			if str(token.value) == "null":
 | 
						|
				token["type"] = "value"
 | 
						|
				token["value"] = null
 | 
						|
			elif tokens[i - 1].type == DialogueConstants.TOKEN_DOT:
 | 
						|
				var caller: Dictionary = tokens[i - 2]
 | 
						|
				var property = token.value
 | 
						|
				if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
					# If the next token is an assignment then we need to leave this as a reference
 | 
						|
					# so that it can be resolved once everything ahead of it has been resolved
 | 
						|
					caller["type"] = "property"
 | 
						|
					caller["property"] = property
 | 
						|
				else:
 | 
						|
					# If we are requesting a deeper property then we need to collapse the
 | 
						|
					# value into the thing we are referencing from
 | 
						|
					caller["type"] = "value"
 | 
						|
					if typeof(caller.value) == TYPE_ARRAY:
 | 
						|
						caller["value"] = caller.value[property]
 | 
						|
					else:
 | 
						|
						caller["value"] = caller.value.get(property)
 | 
						|
				tokens.remove_at(i)
 | 
						|
				tokens.remove_at(i-1)
 | 
						|
				i -= 2
 | 
						|
			elif tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
				# It's a normal variable but we will be assigning to it so don't resolve
 | 
						|
				# it until everything after it has been resolved
 | 
						|
				token["type"] = "variable"
 | 
						|
			else:
 | 
						|
				token["type"] = "value"
 | 
						|
				token["value"] = get_state_value(str(token.value), extra_game_states)
 | 
						|
 | 
						|
		i += 1
 | 
						|
 | 
						|
	# Then multiply and divide
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value)
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			tokens.remove_at(i-1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	# Then addition and subtraction
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["+", "-"]:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value)
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			tokens.remove_at(i-1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	# Then negations
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_NOT:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = not tokens[i+1].value
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	# Then comparisons
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_COMPARISON:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = compare(token.value, tokens[i-1].value, tokens[i+1].value)
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			tokens.remove_at(i-1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	# Then and/or
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_AND_OR:
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value)
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			tokens.remove_at(i-1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	# Lastly, resolve any assignments
 | 
						|
	i = 0
 | 
						|
	limit = 0
 | 
						|
	while i < tokens.size() and limit < 1000:
 | 
						|
		limit += 1
 | 
						|
		var token: Dictionary = tokens[i]
 | 
						|
		if token.type == DialogueConstants.TOKEN_ASSIGNMENT:
 | 
						|
			var lhs: Dictionary = tokens[i - 1]
 | 
						|
			var value
 | 
						|
 | 
						|
			match lhs.type:
 | 
						|
				"variable":
 | 
						|
					value = apply_operation(token.value, get_state_value(lhs.value, extra_game_states), tokens[i+1].value)
 | 
						|
					set_state_value(lhs.value, value, extra_game_states)
 | 
						|
				"property":
 | 
						|
					value = apply_operation(token.value, lhs.value.get(lhs.property), tokens[i+1].value)
 | 
						|
					if typeof(lhs.value) == TYPE_DICTIONARY:
 | 
						|
						lhs.value[lhs.property] = value
 | 
						|
					else:
 | 
						|
						lhs.value.set(lhs.property, value)
 | 
						|
				"dictionary":
 | 
						|
					value = apply_operation(token.value, lhs.value.get(lhs.key, null), tokens[i+1].value)
 | 
						|
					lhs.value[lhs.key] = value
 | 
						|
				"array":
 | 
						|
					assert(lhs.key < lhs.value.size(), DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = lhs.key, array = lhs.value }))
 | 
						|
					value = apply_operation(token.value, lhs.value[lhs.key], tokens[i+1].value)
 | 
						|
					lhs.value[lhs.key] = value
 | 
						|
				_:
 | 
						|
					assert(false, DialogueConstants.translate("runtime.left_hand_size_cannot_be_assigned_to"))
 | 
						|
 | 
						|
			token["type"] = "value"
 | 
						|
			token["value"] = value
 | 
						|
			tokens.remove_at(i+1)
 | 
						|
			tokens.remove_at(i-1)
 | 
						|
			i -= 1
 | 
						|
		i += 1
 | 
						|
 | 
						|
	assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong"))
 | 
						|
 | 
						|
	return tokens[0].value
 | 
						|
 | 
						|
 | 
						|
func compare(operator: String, first_value, second_value) -> bool:
 | 
						|
	match operator:
 | 
						|
		"in":
 | 
						|
			if first_value == null or second_value == null:
 | 
						|
				return false
 | 
						|
			else:
 | 
						|
				return first_value in second_value
 | 
						|
		"<":
 | 
						|
			if first_value == null:
 | 
						|
				return true
 | 
						|
			elif second_value == null:
 | 
						|
				return false
 | 
						|
			else:
 | 
						|
				return first_value < second_value
 | 
						|
		">":
 | 
						|
			if first_value == null:
 | 
						|
				return false
 | 
						|
			elif second_value == null:
 | 
						|
				return true
 | 
						|
			else:
 | 
						|
				return first_value > second_value
 | 
						|
		"<=":
 | 
						|
			if first_value == null:
 | 
						|
				return true
 | 
						|
			elif second_value == null:
 | 
						|
				return false
 | 
						|
			else:
 | 
						|
				return first_value <= second_value
 | 
						|
		">=":
 | 
						|
			if first_value == null:
 | 
						|
				return false
 | 
						|
			elif second_value == null:
 | 
						|
				return true
 | 
						|
			else:
 | 
						|
				return first_value >= second_value
 | 
						|
		"==":
 | 
						|
			if first_value == null:
 | 
						|
				if typeof(second_value) == TYPE_BOOL:
 | 
						|
					return second_value == false
 | 
						|
				else:
 | 
						|
					return false
 | 
						|
			else:
 | 
						|
				return first_value == second_value
 | 
						|
		"!=":
 | 
						|
			if first_value == null:
 | 
						|
				if typeof(second_value) == TYPE_BOOL:
 | 
						|
					return second_value == true
 | 
						|
				else:
 | 
						|
					return false
 | 
						|
			else:
 | 
						|
				return first_value != second_value
 | 
						|
 | 
						|
	return false
 | 
						|
 | 
						|
 | 
						|
func apply_operation(operator: String, first_value, second_value):
 | 
						|
	match operator:
 | 
						|
		"=":
 | 
						|
			return second_value
 | 
						|
		"+", "+=":
 | 
						|
			return first_value + second_value
 | 
						|
		"-", "-=":
 | 
						|
			return first_value - second_value
 | 
						|
		"/", "/=":
 | 
						|
			return first_value / second_value
 | 
						|
		"*", "*=":
 | 
						|
			return first_value * second_value
 | 
						|
		"%":
 | 
						|
			return first_value % second_value
 | 
						|
		"and":
 | 
						|
			return first_value and second_value
 | 
						|
		"or":
 | 
						|
			return first_value or second_value
 | 
						|
 | 
						|
	assert(false, DialogueConstants.translate("runtime.unknown_operator"))
 | 
						|
 | 
						|
 | 
						|
# Check if a dialogue line contains meaningful information
 | 
						|
func is_valid(line: DialogueLine) -> bool:
 | 
						|
	if line == null:
 | 
						|
		return false
 | 
						|
	if line.type == DialogueConstants.TYPE_MUTATION and line.mutation == null:
 | 
						|
		return false
 | 
						|
	if line.type == DialogueConstants.TYPE_RESPONSE and line.responses.size() == 0:
 | 
						|
		return false
 | 
						|
	return true
 | 
						|
 | 
						|
 | 
						|
func thing_has_method(thing, method: String, args: Array) -> bool:
 | 
						|
	match typeof(thing):
 | 
						|
		TYPE_DICTIONARY:
 | 
						|
			return method in SUPPORTED_DICTIONARY_METHODS
 | 
						|
		TYPE_ARRAY:
 | 
						|
			return method in SUPPORTED_ARRAY_METHODS
 | 
						|
 | 
						|
	if method in ["call", "call_deferred"]:
 | 
						|
		return thing.has_method(args[0])
 | 
						|
	else:
 | 
						|
		return thing.has_method(method)
 | 
						|
 | 
						|
 | 
						|
# Check if a given property exists
 | 
						|
func thing_has_property(thing: Object, property: String) -> bool:
 | 
						|
	if thing == null:
 | 
						|
		return false
 | 
						|
 | 
						|
	for p in thing.get_property_list():
 | 
						|
		if _node_properties.has(p.name):
 | 
						|
			# Ignore any properties on the base Node
 | 
						|
			continue
 | 
						|
		if p.name == property:
 | 
						|
			return true
 | 
						|
 | 
						|
	return false
 | 
						|
 | 
						|
 | 
						|
func resolve_array_method(array: Array, method_name: String, args: Array):
 | 
						|
	match method_name:
 | 
						|
		"assign":
 | 
						|
			array.assign(args[0])
 | 
						|
			return null
 | 
						|
		"append":
 | 
						|
			array.append(args[0])
 | 
						|
			return null
 | 
						|
		"append_array":
 | 
						|
			array.append_array(args[0])
 | 
						|
			return null
 | 
						|
		"back":
 | 
						|
			return array.back()
 | 
						|
		"count":
 | 
						|
			return array.count(args[0])
 | 
						|
		"clear":
 | 
						|
			array.clear()
 | 
						|
			return null
 | 
						|
		"erase":
 | 
						|
			array.erase(args[0])
 | 
						|
			return null
 | 
						|
		"has":
 | 
						|
			return array.has(args[0])
 | 
						|
		"insert":
 | 
						|
			return array.insert(args[0], args[1])
 | 
						|
		"is_empty":
 | 
						|
			return array.is_empty()
 | 
						|
		"max":
 | 
						|
			return array.max()
 | 
						|
		"min":
 | 
						|
			return array.min()
 | 
						|
		"pick_random":
 | 
						|
			return array.pick_random()
 | 
						|
		"pop_at":
 | 
						|
			return array.pop_at(args[0])
 | 
						|
		"pop_back":
 | 
						|
			return array.pop_back()
 | 
						|
		"pop_front":
 | 
						|
			return array.pop_front()
 | 
						|
		"push_back":
 | 
						|
			array.push_back(args[0])
 | 
						|
			return null
 | 
						|
		"push_front":
 | 
						|
			array.push_front(args[0])
 | 
						|
			return null
 | 
						|
		"remove_at":
 | 
						|
			array.remove_at(args[0])
 | 
						|
			return null
 | 
						|
		"reverse":
 | 
						|
			array.reverse()
 | 
						|
			return null
 | 
						|
		"shuffle":
 | 
						|
			array.shuffle()
 | 
						|
			return null
 | 
						|
		"size":
 | 
						|
			return array.size()
 | 
						|
		"sort":
 | 
						|
			array.sort()
 | 
						|
			return null
 | 
						|
 | 
						|
	assert(false, DialogueConstants.translate("runtime.unsupported_array_method").format({ method_name = method_name }))
 | 
						|
 | 
						|
 | 
						|
func resolve_dictionary_method(dictionary: Dictionary, method_name: String, args: Array):
 | 
						|
	match method_name:
 | 
						|
		"has":
 | 
						|
			return dictionary.has(args[0])
 | 
						|
		"has_all":
 | 
						|
			return dictionary.has_all(args[0])
 | 
						|
		"get":
 | 
						|
			return dictionary.get(args[0])
 | 
						|
		"keys":
 | 
						|
			return dictionary.keys()
 | 
						|
		"values":
 | 
						|
			return dictionary.values()
 | 
						|
		"size":
 | 
						|
			return dictionary.size()
 | 
						|
 | 
						|
	assert(false, DialogueConstants.translate("runtime.unsupported_dictionary_method").format({ method_name = method_name }))
 |