707 lines
23 KiB
GDScript3
707 lines
23 KiB
GDScript3
|
extends Node
|
||
|
|
||
|
|
||
|
signal dialogue_started
|
||
|
signal dialogue_finished
|
||
|
|
||
|
|
||
|
const DialogueResource = preload("res://addons/dialogue_manager/dialogue_resource.gd")
|
||
|
const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd")
|
||
|
const DialogueLine = preload("res://addons/dialogue_manager/dialogue_line.gd")
|
||
|
const DialogueResponse = preload("res://addons/dialogue_manager/dialogue_response.gd")
|
||
|
|
||
|
const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd")
|
||
|
const DialogueParser = preload("res://addons/dialogue_manager/components/parser.gd")
|
||
|
|
||
|
const ExampleBalloon = preload("res://addons/dialogue_manager/example_balloon/example_balloon.gd")
|
||
|
|
||
|
|
||
|
var resource: DialogueResource
|
||
|
var game_states: Array = []
|
||
|
var auto_translate: bool = true
|
||
|
var settings: DialogueSettings = DialogueSettings.new()
|
||
|
|
||
|
var is_dialogue_running := false setget set_is_dialogue_running
|
||
|
|
||
|
var _node_properties: Array = []
|
||
|
var _resource_cache: Array = []
|
||
|
var _trash: Node = Node.new()
|
||
|
|
||
|
|
||
|
func _ready() -> void:
|
||
|
# Cache the known Node2D properties
|
||
|
_node_properties = ["Script Variables"]
|
||
|
var temp_node = Node2D.new()
|
||
|
for property in temp_node.get_property_list():
|
||
|
_node_properties.append(property.name)
|
||
|
temp_node.free()
|
||
|
|
||
|
# Load the config file (if there is one) so we can set up any global state objects
|
||
|
add_child(settings)
|
||
|
for node_name in settings.get_runtime_value("states", []):
|
||
|
var state = get_node("/root/" + node_name)
|
||
|
if state:
|
||
|
game_states.append(state)
|
||
|
|
||
|
# Add a node for cleaning up
|
||
|
add_child(_trash)
|
||
|
|
||
|
|
||
|
# Step through lines and run any mutations until we either
|
||
|
# hit some dialogue or the end of the conversation
|
||
|
func get_next_dialogue_line(key: String, override_resource: DialogueResource = null) -> DialogueLine:
|
||
|
cleanup()
|
||
|
|
||
|
# Fix up any keys that have spaces in them
|
||
|
key = key.replace(" ", "_").strip_edges()
|
||
|
|
||
|
# You have to provide a dialogue resource
|
||
|
assert(resource != null or override_resource != null, "No dialogue resource provided")
|
||
|
|
||
|
var local_resource: DialogueResource = (override_resource if override_resource != null else resource)
|
||
|
|
||
|
assert(local_resource.syntax_version == DialogueConstants.SYNTAX_VERSION, "This dialogue resource is older than the runtime expects.")
|
||
|
|
||
|
var resource_path = local_resource.resource_path
|
||
|
if local_resource.lines.size() == 0:
|
||
|
# We probably have pre-baking turned off so we need to compile on the fly
|
||
|
local_resource = compile_resource(local_resource)
|
||
|
|
||
|
if local_resource.errors.size() > 0:
|
||
|
# Store in a local var for debugger convenience
|
||
|
var errors = local_resource.errors
|
||
|
printerr("There are %d error(s) in %s" % [errors.size(), resource_path])
|
||
|
for error in errors:
|
||
|
printerr("\tLine %s: %s" % [error.get("line"), error.get("message")])
|
||
|
assert(false, "The provided DialogueResource contains errors. See Output for details.")
|
||
|
|
||
|
var dialogue = get_line(key, local_resource)
|
||
|
|
||
|
yield(get_tree(), "idle_frame")
|
||
|
|
||
|
self.is_dialogue_running = true
|
||
|
|
||
|
# If our dialogue is nothing then we hit the end
|
||
|
if dialogue == null or not is_valid(dialogue):
|
||
|
self.is_dialogue_running = false
|
||
|
return null
|
||
|
|
||
|
# Run the mutation if it is one
|
||
|
if dialogue.type == DialogueConstants.TYPE_MUTATION:
|
||
|
yield(mutate(dialogue.mutation), "completed")
|
||
|
dialogue.queue_free()
|
||
|
if dialogue.next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]:
|
||
|
# End the conversation
|
||
|
self.is_dialogue_running = false
|
||
|
return null
|
||
|
else:
|
||
|
return get_next_dialogue_line(dialogue.next_id, local_resource)
|
||
|
else:
|
||
|
return dialogue
|
||
|
|
||
|
|
||
|
func replace_values(line_or_response) -> String:
|
||
|
if line_or_response is DialogueLine:
|
||
|
var line: DialogueLine = line_or_response
|
||
|
return get_with_replacements(line.dialogue, line.replacements)
|
||
|
elif line_or_response is DialogueResponse:
|
||
|
var response: DialogueResponse = line_or_response
|
||
|
return get_with_replacements(response.prompt, response.replacements)
|
||
|
else:
|
||
|
return ""
|
||
|
|
||
|
|
||
|
func get_resource_from_text(text: String) -> DialogueResource:
|
||
|
var parser = DialogueParser.new()
|
||
|
var new_resource = DialogueResource.new()
|
||
|
|
||
|
var results = parser.parse(text)
|
||
|
parser.queue_free()
|
||
|
|
||
|
new_resource.raw_text = text
|
||
|
new_resource.syntax_version = DialogueConstants.SYNTAX_VERSION
|
||
|
new_resource.titles = results.get("titles")
|
||
|
new_resource.lines = results.get("lines")
|
||
|
new_resource.errors = results.get("errors")
|
||
|
|
||
|
return new_resource
|
||
|
|
||
|
|
||
|
func show_example_dialogue_balloon(title: String, local_resource: DialogueResource = null) -> void:
|
||
|
var dialogue = yield(get_next_dialogue_line(title, local_resource), "completed")
|
||
|
if dialogue != null:
|
||
|
var balloon = preload("res://addons/dialogue_manager/example_balloon/example_balloon.tscn").instance()
|
||
|
balloon.dialogue = dialogue
|
||
|
get_tree().current_scene.add_child(balloon)
|
||
|
show_example_dialogue_balloon(yield(balloon, "actioned"), local_resource)
|
||
|
|
||
|
|
||
|
### Helpers
|
||
|
|
||
|
|
||
|
func compile_resource(resource: DialogueResource) -> DialogueResource:
|
||
|
# See if we have this cached, first
|
||
|
for item in _resource_cache:
|
||
|
if item[0] == resource.resource_path:
|
||
|
return item[1]
|
||
|
|
||
|
# Otherwise, compile it and then cache it
|
||
|
var next_resource = get_resource_from_text(resource.raw_text)
|
||
|
_resource_cache.insert(0, [resource.resource_path, next_resource])
|
||
|
|
||
|
# Only keep recent stuff in the cache
|
||
|
if _resource_cache.size() > 5:
|
||
|
_resource_cache.remove(5)
|
||
|
|
||
|
return next_resource
|
||
|
|
||
|
|
||
|
# Get a line by its ID
|
||
|
func get_line(key: String, local_resource: DialogueResource) -> DialogueLine:
|
||
|
# End of conversation
|
||
|
if key in [DialogueConstants.ID_NULL, DialogueConstants.ID_END_CONVERSATION, null]:
|
||
|
return null
|
||
|
|
||
|
# See if it is a title
|
||
|
if key.begins_with("~ "):
|
||
|
key = key.substr(2)
|
||
|
if local_resource.titles.has(key):
|
||
|
key = local_resource.titles.get(key)
|
||
|
|
||
|
# Key not found
|
||
|
if not local_resource.lines.has(key):
|
||
|
printerr("Line for key \"%s\" could not be found in %s" % [key, local_resource.resource_path])
|
||
|
assert(false, "The provided DialogueResource does not contain that line key. See Output for details.")
|
||
|
|
||
|
var data = local_resource.lines.get(key)
|
||
|
|
||
|
# Check condtiions
|
||
|
if data.get("type") == DialogueConstants.TYPE_CONDITION:
|
||
|
# "else" will have no actual condition
|
||
|
if data.get("condition") == null or check(data.get("condition")):
|
||
|
return get_line(data.get("next_id"), local_resource)
|
||
|
else:
|
||
|
return get_line(data.get("next_conditional_id"), local_resource)
|
||
|
|
||
|
# Evaluate early exits
|
||
|
if data.get("type") == DialogueConstants.TYPE_GOTO:
|
||
|
return get_line(data.get("next_id"), local_resource)
|
||
|
|
||
|
# Set up a line object
|
||
|
var line = DialogueLine.new(data, auto_translate)
|
||
|
line.dialogue_manager = self
|
||
|
|
||
|
# If we are the first of a list of responses then get the other ones
|
||
|
if data.get("type") == DialogueConstants.TYPE_RESPONSE:
|
||
|
line.responses = get_responses(data.get("responses"), local_resource)
|
||
|
return line
|
||
|
|
||
|
# Add as a child so that it gets cleaned up automatically
|
||
|
_trash.add_child(line)
|
||
|
|
||
|
# Replace any variables in the dialogue text
|
||
|
if data.get("type") == DialogueConstants.TYPE_DIALOGUE and data.has("replacements"):
|
||
|
line.character = get_with_replacements(line.character, line.character_replacements)
|
||
|
line.dialogue = get_with_replacements(line.dialogue, line.replacements)
|
||
|
|
||
|
# Inject the next node's responses if they have any
|
||
|
var next_line = local_resource.lines.get(line.next_id)
|
||
|
if next_line != null and next_line.get("type") == DialogueConstants.TYPE_RESPONSE:
|
||
|
line.responses = get_responses(next_line.get("responses"), local_resource)
|
||
|
|
||
|
return line
|
||
|
|
||
|
|
||
|
func set_is_dialogue_running(value: bool) -> void:
|
||
|
if is_dialogue_running != value:
|
||
|
if value:
|
||
|
emit_signal("dialogue_started")
|
||
|
else:
|
||
|
emit_signal("dialogue_finished")
|
||
|
|
||
|
is_dialogue_running = value
|
||
|
|
||
|
|
||
|
func get_game_states() -> Array:
|
||
|
var current_scene = get_tree().current_scene
|
||
|
var unique_states = []
|
||
|
for state in [current_scene] + game_states:
|
||
|
if not unique_states.has(state):
|
||
|
unique_states.append(state)
|
||
|
return unique_states
|
||
|
|
||
|
|
||
|
# Check if a condition is met
|
||
|
func check(condition: Dictionary) -> bool:
|
||
|
if condition.size() == 0: return true
|
||
|
|
||
|
return resolve(condition.get("expression").duplicate(true))
|
||
|
|
||
|
|
||
|
# Make a change to game state or run a method
|
||
|
func mutate(mutation: Dictionary) -> void:
|
||
|
if mutation == null: return
|
||
|
|
||
|
if mutation.has("function"):
|
||
|
# If lhs is a function then we run it and return because you can't assign to a function
|
||
|
var function_name = mutation.get("function")
|
||
|
var args = resolve_each(mutation.get("args"))
|
||
|
match function_name:
|
||
|
"wait":
|
||
|
yield(get_tree().create_timer(float(args[0])), "timeout")
|
||
|
return
|
||
|
"emit":
|
||
|
for state in get_game_states():
|
||
|
if 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])
|
||
|
"debug":
|
||
|
var printable = {}
|
||
|
for i in range(args.size()):
|
||
|
printable[mutation.get("args")[i][0].get("value")] = args[i]
|
||
|
print(printable)
|
||
|
_:
|
||
|
for state in get_game_states():
|
||
|
if state.has_method(function_name):
|
||
|
var result = state.callv(function_name, args)
|
||
|
if result is GDScriptFunctionState and result.is_valid():
|
||
|
yield(result, "completed")
|
||
|
else:
|
||
|
yield(get_tree(), "idle_frame")
|
||
|
return
|
||
|
|
||
|
printerr("'" + function_name + "' is not a method in any game states (" + str(get_game_states()) + ").")
|
||
|
assert(false, "Missing function on current scene or game state. See Output for details.")
|
||
|
|
||
|
elif mutation.has("expression"):
|
||
|
resolve(mutation.get("expression").duplicate(true))
|
||
|
|
||
|
# Wait one frame to give the dialogue handler a chance to yield
|
||
|
yield(get_tree(), "idle_frame")
|
||
|
|
||
|
|
||
|
func resolve_each(array: Array) -> Array:
|
||
|
var results = []
|
||
|
for item in array:
|
||
|
results.append(resolve(item.duplicate(true)))
|
||
|
return results
|
||
|
|
||
|
|
||
|
# Replace any variables, etc in the dialogue with their state values
|
||
|
func get_with_replacements(text: String, replacements: Array) -> String:
|
||
|
for replacement in replacements:
|
||
|
var value = resolve(replacement.get("expression").duplicate(true))
|
||
|
text = text.replace(replacement.get("value_in_text"), str(value))
|
||
|
|
||
|
return text
|
||
|
|
||
|
|
||
|
# Replace an array of line IDs with their response prompts
|
||
|
func get_responses(ids: Array, local_resource: DialogueResource) -> Array:
|
||
|
var responses: Array = []
|
||
|
for id in ids:
|
||
|
var data = local_resource.lines.get(id)
|
||
|
if settings.get_runtime_value("include_all_responses", false) or data.get("condition") == null or check(data.get("condition")):
|
||
|
var response = DialogueResponse.new(data, auto_translate)
|
||
|
response.character = get_with_replacements(response.character, response.character_replacements)
|
||
|
response.prompt = get_with_replacements(response.prompt, response.replacements)
|
||
|
response.is_allowed = data.get("condition") == null or check(data.get("condition"))
|
||
|
# Add as a child so that it gets cleaned up automatically
|
||
|
_trash.add_child(response)
|
||
|
responses.append(response)
|
||
|
|
||
|
return responses
|
||
|
|
||
|
|
||
|
# Get a value on the current scene or game state
|
||
|
func get_state_value(property: String):
|
||
|
# It's a variable
|
||
|
for state in get_game_states():
|
||
|
if has_property(state, property):
|
||
|
return state.get(property)
|
||
|
|
||
|
printerr("'" + property + "' is not a property on any game states (" + str(get_game_states()) + ").")
|
||
|
assert(false, "Missing property on current scene or game state. See Output for details.")
|
||
|
|
||
|
|
||
|
# Set a value on the current scene or game state
|
||
|
func set_state_value(property: String, value) -> void:
|
||
|
for state in get_game_states():
|
||
|
if has_property(state, property):
|
||
|
state.set(property, value)
|
||
|
return
|
||
|
|
||
|
printerr("'" + property + "' is not a property on any game states (" + str(get_game_states()) + ").")
|
||
|
assert(false, "Missing property on current scene or game state. See Output for details.")
|
||
|
|
||
|
|
||
|
# Collapse any expressions
|
||
|
func resolve(tokens: Array):
|
||
|
# Handle groups first
|
||
|
for token in tokens:
|
||
|
if token.get("type") == DialogueConstants.TOKEN_GROUP:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = resolve(token.get("value"))
|
||
|
|
||
|
# Then variables/methods
|
||
|
var i = 0
|
||
|
var limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
var token = tokens[i]
|
||
|
|
||
|
if token.get("type") == DialogueConstants.TOKEN_FUNCTION:
|
||
|
var function_name = token.get("function")
|
||
|
var args = resolve_each(token.get("value"))
|
||
|
if function_name == "str":
|
||
|
token["type"] = "value"
|
||
|
token["value"] = str(args[0])
|
||
|
elif tokens[i - 1].get("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 = tokens[i - 2]
|
||
|
if not caller.get("value").has_method(function_name):
|
||
|
printerr("\"%s\" is not a callable method on \"%s\"" % [function_name, str(caller)])
|
||
|
assert(false, "Missing callable method on calling object. See Output for details.")
|
||
|
caller["type"] = "value"
|
||
|
caller["value"] = caller.get("value").callv(function_name, args)
|
||
|
tokens.remove(i)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 2
|
||
|
else:
|
||
|
var found = false
|
||
|
for state in get_game_states():
|
||
|
if state.has_method(function_name):
|
||
|
token["type"] = "value"
|
||
|
token["value"] = state.callv(function_name, args)
|
||
|
found = true
|
||
|
|
||
|
if not found:
|
||
|
printerr("\"%s\" is not a method on any game states (%s)" % [function_name, str(get_game_states())])
|
||
|
assert(false, "Missing function on current scene or game state. See Output for details.")
|
||
|
|
||
|
elif token.get("type") == DialogueConstants.TOKEN_DICTIONARY_REFERENCE:
|
||
|
var value = get_state_value(token.get("variable"))
|
||
|
var index = resolve(token.get("value"))
|
||
|
if typeof(value) == TYPE_DICTIONARY:
|
||
|
if tokens.size() > i + 1 and tokens[i + 1].get("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:
|
||
|
printerr("Key \"%s\" not found in dictionary \"%s\"" % [str(index), token.get("variable")])
|
||
|
assert(false, "Key not found in dictionary. See Output for details.")
|
||
|
elif typeof(value) == TYPE_ARRAY:
|
||
|
if tokens.size() > i + 1 and tokens[i + 1].get("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:
|
||
|
printerr("Index %d out of bounds of array \"%s\"" % [index, token.get("variable")])
|
||
|
assert(false, "Index out of bounds of array. See Output for details.")
|
||
|
|
||
|
elif token.get("type") == DialogueConstants.TOKEN_ARRAY:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = resolve_each(token.get("value"))
|
||
|
|
||
|
elif token.get("type") == DialogueConstants.TOKEN_DICTIONARY:
|
||
|
token["type"] = "value"
|
||
|
var dictionary = {}
|
||
|
for key in token.get("value").keys():
|
||
|
var resolved_key = resolve([key])
|
||
|
var resolved_value = resolve([token.get("value").get(key)])
|
||
|
dictionary[resolved_key] = resolved_value
|
||
|
token["value"] = dictionary
|
||
|
|
||
|
elif token.get("type") == DialogueConstants.TOKEN_VARIABLE:
|
||
|
if token.get("value") == "null":
|
||
|
token["type"] = "value"
|
||
|
token["value"] = null
|
||
|
elif tokens[i - 1].get("type") == DialogueConstants.TOKEN_DOT:
|
||
|
var caller = tokens[i - 2]
|
||
|
var property = token.get("value")
|
||
|
if tokens.size() > i + 1 and tokens[i + 1].get("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"
|
||
|
caller["value"] = caller.get("value").get(property)
|
||
|
tokens.remove(i)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 2
|
||
|
elif tokens.size() > i + 1 and tokens[i + 1].get("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(token.get("value"))
|
||
|
|
||
|
i += 1
|
||
|
|
||
|
# Then multiply and divide
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_OPERATOR and token.get("value") in ["*", "/", "%"]:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = apply_operation(token.get("value"), tokens[i-1].get("value"), tokens[i+1].get("value"))
|
||
|
tokens.remove(i+1)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
# Then addition and subtraction
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_OPERATOR and token.get("value") in ["+", "-"]:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = apply_operation(token.get("value"), tokens[i-1].get("value"), tokens[i+1].get("value"))
|
||
|
tokens.remove(i+1)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
# Then negations
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_NOT:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = not tokens[i+1].get("value")
|
||
|
tokens.remove(i+1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
# Then comparisons
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_COMPARISON:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = compare(token.get("value"), tokens[i-1].get("value"), tokens[i+1].get("value"))
|
||
|
tokens.remove(i+1)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
# Then and/or
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_AND_OR:
|
||
|
token["type"] = "value"
|
||
|
token["value"] = apply_operation(token.get("value"), tokens[i-1].get("value"), tokens[i+1].get("value"))
|
||
|
tokens.remove(i+1)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
# Lastly, resolve any assignments
|
||
|
i = 0
|
||
|
limit = 0
|
||
|
while i < tokens.size() and limit < 1000:
|
||
|
limit += 1
|
||
|
var token = tokens[i]
|
||
|
if token.get("type") == DialogueConstants.TOKEN_ASSIGNMENT:
|
||
|
var lhs = tokens[i - 1]
|
||
|
var value
|
||
|
|
||
|
match lhs.get("type"):
|
||
|
"variable":
|
||
|
value = apply_operation(token.get("value"), get_state_value(lhs.get("value")), tokens[i+1].get("value"))
|
||
|
set_state_value(lhs.get("value"), value)
|
||
|
"property":
|
||
|
value = apply_operation(token.get("value"), lhs.get("value").get(lhs.get("property")), tokens[i+1].get("value"))
|
||
|
lhs.get("value").set(lhs.get("property"), value)
|
||
|
"dictionary", "array":
|
||
|
value = apply_operation(token.get("value"), lhs.get("value")[lhs.get("key")], tokens[i+1].get("value"))
|
||
|
lhs.get("value")[lhs.get("key")] = value
|
||
|
_:
|
||
|
assert(false, "Unknown assignment target")
|
||
|
|
||
|
token["type"] = "value"
|
||
|
token["value"] = value
|
||
|
tokens.remove(i+1)
|
||
|
tokens.remove(i-1)
|
||
|
i -= 1
|
||
|
i += 1
|
||
|
|
||
|
if limit >= 1000:
|
||
|
assert(false, "Something went wrong")
|
||
|
|
||
|
return tokens[0].get("value")
|
||
|
|
||
|
|
||
|
func compare(operator: String, first_value, second_value):
|
||
|
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
|
||
|
|
||
|
|
||
|
func apply_operation(operator: String, first_value, second_value):
|
||
|
if first_value == null:
|
||
|
if typeof(second_value) == TYPE_BOOL and second_value == true:
|
||
|
return false
|
||
|
else:
|
||
|
return second_value
|
||
|
elif second_value == null:
|
||
|
if typeof(first_value) == TYPE_BOOL and first_value == true:
|
||
|
return false
|
||
|
else:
|
||
|
return first_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, "Unknown operator")
|
||
|
|
||
|
|
||
|
# Check if a dialogue line contains meaninful information
|
||
|
func is_valid(line: DialogueLine) -> bool:
|
||
|
if line.type == DialogueConstants.TYPE_DIALOGUE and line.dialogue == "":
|
||
|
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
|
||
|
|
||
|
|
||
|
# Check if a given property exists
|
||
|
func has_property(thing: Object, name: 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 == name:
|
||
|
return true
|
||
|
|
||
|
return false
|
||
|
|
||
|
|
||
|
func cleanup() -> void:
|
||
|
for line in _trash.get_children():
|
||
|
line.queue_free()
|