ESC compiler rewrite

Splits the former ESC_Runner and ESC_Level_Runner into multiple dedicated managers. 
Authored-by: Dennis Ploeger <develop@dieploegers.de>
This commit is contained in:
Dennis Ploeger
2021-06-04 16:12:42 +02:00
committed by GitHub
parent f069ab2ffd
commit 746a724f5a
115 changed files with 4740 additions and 2584 deletions

View File

@@ -133,6 +133,16 @@ func set_escoria_main_settings():
ProjectSettings.set_setting("application/run/main_scene", "res://addons/escoria-core/game/main_scene.tscn") ProjectSettings.set_setting("application/run/main_scene", "res://addons/escoria-core/game/main_scene.tscn")
if not ProjectSettings.has_setting("escoria/main/command_directories"):
ProjectSettings.set_setting("escoria/main/command_directories", [
"res://addons/escoria-core/game/core-scripts/esc/commands"
])
ProjectSettings.add_property_info({
"name": "escoria/main/command_directories",
"type": TYPE_ARRAY,
})
if !ProjectSettings.has_setting("escoria/main/text_lang"): if !ProjectSettings.has_setting("escoria/main/text_lang"):
ProjectSettings.set_setting("escoria/main/text_lang", TranslationServer.get_locale()) ProjectSettings.set_setting("escoria/main/text_lang", TranslationServer.get_locale())
var text_lang_property_info = { var text_lang_property_info = {
@@ -168,6 +178,17 @@ func set_escoria_debug_settings():
if !ProjectSettings.has_setting("escoria/debug/development_lang"): if !ProjectSettings.has_setting("escoria/debug/development_lang"):
ProjectSettings.set_setting("escoria/debug/development_lang", "en") ProjectSettings.set_setting("escoria/debug/development_lang", "en")
# Assure log level preference
if not ProjectSettings.has_setting("escoria/debug/log_level"):
ProjectSettings.set_setting("escoria/debug/log_level", "ERROR")
var property_info = {
"name": "escoria/debug/log_level",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "ERROR,WARNING,INFO,DEBUG"
}
ProjectSettings.add_property_info(property_info)
func set_escoria_internal_settings(): func set_escoria_internal_settings():
if !ProjectSettings.has_setting("escoria/internals/save_data"): if !ProjectSettings.has_setting("escoria/internals/save_data"):

View File

@@ -198,7 +198,9 @@ func walk_stop(pos):
# orient towards the defined interaction direction set on the object (if any) # orient towards the defined interaction direction set on the object (if any)
if walk_context.has("target_object") \ if walk_context.has("target_object") \
and walk_context.target_object.player_orients_on_arrival \ and walk_context.target_object.player_orients_on_arrival \
and escoria.esc_runner.get_interactive(walk_context.target_object.global_id): and escoria.object_manager.get_object(
walk_context.target_object.global_id
).interactive:
var orientation = walk_context["target_object"].interaction_direction var orientation = walk_context["target_object"].interaction_direction
last_dir = orientation last_dir = orientation
parent.animation_sprite.play(parent.animations.idles[orientation][0]) parent.animation_sprite.play(parent.animations.idles[orientation][0])
@@ -209,11 +211,6 @@ func walk_stop(pos):
pose_scale = parent.animations.idles[last_dir][1] pose_scale = parent.animations.idles[last_dir][1]
update_terrain() update_terrain()
if walk_context != null:
escoria.esc_level_runner.finished(walk_context)
escoria.esc_level_runner.finished()
# walk_context = null
yield(parent.animation_sprite, "animation_finished") yield(parent.animation_sprite, "animation_finished")
escoria.logger.info(parent.global_id + " arrived at " + str(walk_context)) escoria.logger.info(parent.global_id + " arrived at " + str(walk_context))
parent.emit_signal("arrived", walk_context) parent.emit_signal("arrived", walk_context)

View File

@@ -0,0 +1,295 @@
extends Control
func _test_basic() -> bool:
var esc = """
:test
# first group
>
say player "Test"
# Second group
> [test]
say player "Test2 BLANK"
say player "Test3" [test2]
# Third group
>
say player "Test4"
# Fourth group
>
say player "Test5"
say player "Test 6"
"""
var script = escoria.esc_compiler.compile(esc.split("\n"))
var subject = script
assert(subject is ESCScript)
subject = script.events.keys()
assert(subject.size() == 1)
assert(subject[0] == "test")
subject = script.events["test"].statements
assert(subject.size() == 2)
subject = script.events["test"].statements[0]
assert(subject is ESCGroup)
assert(subject.statements.size() == 4)
subject = script.events["test"].statements[0].statements[0]
assert(subject is ESCCommand)
assert(subject.name == "say")
assert(subject.parameters.size() == 2)
assert(subject.parameters[0] == "player")
assert(subject.parameters[1] == "Test")
subject = script.events["test"].statements[0].statements[1]
assert(subject is ESCGroup)
assert(subject.conditions.size() == 1)
assert(subject.conditions[0] is ESCCondition)
assert(subject.conditions[0].flag == "test")
subject = script.events["test"].statements[0].statements[1].statements[0]
assert(subject is ESCCommand)
assert(subject.name == "say")
assert(subject.parameters.size() == 2)
assert(subject.parameters[0] == "player")
assert(subject.parameters[1] == "Test2 BLANK")
subject = script.events["test"].statements[0].statements[2]
assert(subject is ESCCommand)
assert(subject.name == "say")
assert(subject.parameters.size() == 2)
assert(subject.parameters[0] == "player")
assert(subject.parameters[1] == "Test3")
assert(subject.conditions.size() == 1)
assert(subject.conditions[0].flag == "test2")
subject = script.events["test"].statements[0].statements[3]
assert(subject is ESCGroup)
assert(subject.statements.size() == 1)
subject = script.events["test"].statements[1]
assert(subject is ESCGroup)
assert(subject.statements.size() == 2)
subject = script.events["test"].statements[1].statements[1]
assert(subject is ESCCommand)
assert(subject.name == "say")
assert(subject.parameters[1] == "Test 6")
return true
func _test_conditions() -> bool:
var esc = """
:test
say player "Test" [flag]
say player "Test" [flag1,flag2]
say player "Test" [!flag]
say player "Test" [i/flag]
say player "Test" [i/flag,flag]
say player "Test" [i/flag,flag,!flag2]
say player "Test" [eq flag 3]
say player "Test" [eq flag 3,gt flag 5]
say player "Test" [!eq flag 3]
"""
var script = escoria.esc_compiler.compile(esc.split("\n"))
var subject = script.events["test"].statements[0]
assert(subject is ESCCommand)
assert(subject.conditions.size() == 1)
subject = script.events["test"].statements[0].conditions[0]
assert(subject.flag == "flag")
assert(not subject.negated)
assert(not subject.inventory)
assert(subject.comparison == ESCCondition.COMPARISON_NONE)
subject = script.events["test"].statements[1].conditions
assert(subject.size() == 2)
assert(subject[0].flag == "flag1")
assert(subject[1].flag == "flag2")
subject = script.events["test"].statements[2].conditions
assert(subject.size() == 1)
assert(subject[0].flag == "flag")
assert(subject[0].negated)
subject = script.events["test"].statements[3].conditions
assert(subject.size() == 1)
assert(subject[0].flag == "flag")
assert(subject[0].inventory)
subject = script.events["test"].statements[4].conditions
assert(subject.size() == 2)
assert(subject[0].flag == "flag")
assert(subject[0].inventory)
assert(subject[1].flag == "flag")
assert(not subject[1].inventory)
subject = script.events["test"].statements[5].conditions
assert(subject.size() == 3)
assert(subject[0].flag == "flag")
assert(subject[0].inventory)
assert(subject[1].flag == "flag")
assert(not subject[1].inventory)
assert(subject[2].flag == "flag2")
assert(not subject[2].inventory)
assert(subject[2].negated)
subject = script.events["test"].statements[6].conditions
assert(subject.size() == 1)
assert(subject[0].flag == "flag")
assert(subject[0].comparison == ESCCondition.COMPARISON_EQ)
assert(subject[0].comparison_value == 3)
subject = script.events["test"].statements[7].conditions
assert(subject.size() == 2)
assert(subject[0].flag == "flag")
assert(subject[0].comparison == ESCCondition.COMPARISON_EQ)
assert(subject[0].comparison_value == 3)
assert(subject[1].flag == "flag")
assert(subject[1].comparison == ESCCondition.COMPARISON_GT)
assert(subject[1].comparison_value == 5)
subject = script.events["test"].statements[8].conditions
assert(subject.size() == 1)
assert(subject[0].flag == "flag")
assert(subject[0].comparison == ESCCondition.COMPARISON_EQ)
assert(subject[0].comparison_value == 3)
assert(subject[0].negated)
return true
func _test_event_flags() -> bool:
var esc = """
:test | TK
:test2 | TK NO_TT
:test3 | TK NO_TT NO_HUD
:test4 | TK NO_TT CUT_BLACK
"""
var script = escoria.esc_compiler.compile(esc.split("\n"))
var subject = script.events
assert(subject.keys().size() == 4)
assert("test" in subject.keys())
assert("test2" in subject.keys())
assert("test3" in subject.keys())
assert("test4" in subject.keys())
subject = script.events["test"]
assert(subject.name == "test")
assert(subject.flags & ESCEvent.FLAG_TK != 0)
assert(subject.flags & ESCEvent.FLAG_NO_TT == 0)
subject = script.events["test2"]
assert(subject.name == "test2")
assert(subject.flags & ESCEvent.FLAG_TK != 0)
assert(subject.flags & ESCEvent.FLAG_NO_TT != 0)
subject = script.events["test3"]
assert(subject.name == "test3")
assert(subject.flags & ESCEvent.FLAG_TK != 0)
assert(subject.flags & ESCEvent.FLAG_NO_TT != 0)
assert(subject.flags & ESCEvent.FLAG_NO_HUD != 0)
subject = script.events["test4"]
assert(subject.name == "test4")
assert(subject.flags & ESCEvent.FLAG_TK != 0)
assert(subject.flags & ESCEvent.FLAG_NO_TT != 0)
assert(subject.flags & ESCEvent.FLAG_NO_HUD == 0)
assert(subject.flags & ESCEvent.FLAG_CUT_BLACK != 0)
return true
func _test_dialog() -> bool:
var esc = """
:test
?
- "Option 1"
say player "test"
say player "testb"
say player "testb?"
- "Option 2" [flag]
say player "test2"
?
- "Suboption 1"
say player "test21"
- "Suboption 2"
say player "test22"
!
- "Option 3"
>
say player "test3"
!
"""
var script = escoria.esc_compiler.compile(esc.split("\n"))
var subject = script.events["test"].statements
assert(subject.size() == 1)
assert(subject[0] is ESCDialog)
assert(subject[0].options.size() == 3)
subject = script.events["test"].statements[0].options[0]
assert(subject is ESCDialogOption)
assert(subject.option == "Option 1")
subject = script.events["test"].statements[0].options[0].statements
assert(subject.size() == 3)
assert(subject[0] is ESCCommand)
assert(subject[0].name == "say")
assert(subject[0].parameters.size() == 2)
assert(subject[1] is ESCCommand)
assert(subject[1].name == "say")
assert(subject[1].parameters.size() == 2)
assert(subject[1].parameters[1] == "testb")
assert(subject[2] is ESCCommand)
assert(subject[2].name == "say")
assert(subject[2].parameters.size() == 2)
assert(subject[2].parameters[1] == "testb?")
subject = script.events["test"].statements[0].options[1]
assert(subject is ESCDialogOption)
assert(subject.option == "Option 2")
assert(subject.conditions.size() == 1)
assert(subject.conditions[0].flag == "flag")
subject = script.events["test"].statements[0].options[1].statements
assert(subject.size() == 2)
assert(subject[0] is ESCCommand)
assert(subject[0].name == "say")
assert(subject[0].parameters.size() == 2)
assert(subject[1] is ESCDialog)
assert(subject[1].options.size() == 2)
subject = script.events["test"].statements[0].options[2]
assert(subject is ESCDialogOption)
assert(subject.option == "Option 3")
subject = script.events["test"].statements[0].options[2].statements
assert(subject.size() == 1)
assert(subject[0] is ESCGroup)
assert(subject[0].statements.size() == 1)
assert(subject[0].statements[0] is ESCCommand)
assert(subject[0].statements[0].parameters.size() == 2)
return true
func _on_BasicFunctionality_pressed():
$VBoxContainer/VBoxContainer/BasicFunctionality.pressed = self._test_basic()
func _on_Conditions_pressed():
$VBoxContainer/VBoxContainer/Conditions.pressed = self._test_conditions()
func _on_EventFlags_pressed():
$VBoxContainer/VBoxContainer/EventFlags.pressed = self._test_event_flags()
func _on_Dialog_pressed():
$VBoxContainer/VBoxContainer/Dialog.pressed = self._test_dialog()

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc/_test/test_esc_compiler.gd" type="Script" id=1]
[node name="Testsuite" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = 1.0
margin_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"]
margin_right = 1281.0
margin_bottom = 172.0
[node name="BasicFunctionality" type="CheckButton" parent="VBoxContainer/VBoxContainer"]
margin_right = 1281.0
margin_bottom = 40.0
text = "Basic Functionality"
align = 1
[node name="Conditions" type="CheckButton" parent="VBoxContainer/VBoxContainer"]
margin_top = 44.0
margin_right = 1281.0
margin_bottom = 84.0
text = "Check conditions"
align = 1
[node name="EventFlags" type="CheckButton" parent="VBoxContainer/VBoxContainer"]
margin_top = 88.0
margin_right = 1281.0
margin_bottom = 128.0
text = "Check event flags"
align = 1
[node name="Dialog" type="CheckButton" parent="VBoxContainer/VBoxContainer"]
margin_top = 132.0
margin_right = 1281.0
margin_bottom = 172.0
text = "Check dialogs"
align = 1
[connection signal="pressed" from="VBoxContainer/VBoxContainer/BasicFunctionality" to="." method="_on_BasicFunctionality_pressed"]
[connection signal="pressed" from="VBoxContainer/VBoxContainer/Conditions" to="." method="_on_Conditions_pressed"]
[connection signal="pressed" from="VBoxContainer/VBoxContainer/EventFlags" to="." method="_on_EventFlags_pressed"]
[connection signal="pressed" from="VBoxContainer/VBoxContainer/Dialog" to="." method="_on_Dialog_pressed"]

View File

@@ -0,0 +1,47 @@
# `accept_input [ALL|NONE|SKIP]`
#
# What type of input does the game accept. ALL is the default, SKIP allows
# skipping of dialog but nothing else, NONE denies all input. Including opening
# the menu etc. SKIP and NONE also disable autosaves.
#
# *Note* that SKIP gets reset to ALL when the event is done, but NONE persists.
# This allows you to create cut scenes with SKIP where the dialog can be
# skipped, but also initiate locked#### down cutscenes with accept_input
# NONE in :setup and accept_input ALL later in :ready.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name AcceptInputCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING],
["ALL"]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not arguments[0] in ["ALL", "NONE", "SKIP"]:
escoria.logger.report_errors(
"accept_input: invalid parameter",
[
"%s is not a valid parameter value (ALL, NONE, SKIP)" %\
arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"accept_input: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,46 @@
# `anim object name [reverse]`
#
# Executes the animation specificed with the "name" parameter on the object,
# without blocking. The next command in the event will be executed immediately
# after. Optional parameters:
#
# * reverse: plays the animation in reverse when true
#
# @ESC
extends ESCBaseCommand
class_name AnimCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, null, false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"anim: invalid object",
[
"Object with global id %s not found." % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
var obj = escoria.object_manager.objects[command_params[0]]
var anim_id = command_params[1]
var reverse = command_params[2]
var animator = (obj.node as ESCItem).get_animation_player()
if reverse:
animator.play_backwards(anim_id)
else:
animator.play(anim_id)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,44 @@
# `camera_push target [time] [type]`
#
# Push camera to `target`. Target must have camera_pos set. If it's of type
# Camera2D, its zoom will be used as well as position. `type` is any of the
# Tween.TransitionType values without the prefix, eg. LINEAR, QUART or CIRC;
# defaults to QUART. A `time` value of 0 will set the camera immediately.
#
# @ESC
extends ESCBaseCommand
class_name CameraPushCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING, [TYPE_REAL, TYPE_INT], TYPE_STRING],
[null, 1, "QUAD"]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"camera_push: invalid object",
[
"Object global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.push(
escoria.object_manager.get_object(command_params[0]).node,
command_params[1],
command_params[2]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,40 @@
# `camera_set_limits camlimits_id`
# Sets the camera limits to the one defined under `camlimits_id` in ESCRoom's
# camera_limits array.
# - camlimits_id : int : id of the camera limits to apply (defined in ESCRoom's
# camera_limits array)
# @ESC
extends ESCBaseCommand
class_name CameraSetLimitsCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_INT],
[null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.main.current_scene.camera_limits.size() < arguments[0]:
escoria.logger.report_errors(
"camera_set_limits: invalid limits id",
[
"Limit id %d is bigger than limits array size %d" % [
arguments[0],
escoria.main.current_scene.camera_limits.size()
]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.main.set_camera_limits(command_params[0])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,28 @@
# `camera_set_pos speed x y`
#
# Moves the camera to a position defined by "x" and "y", at the speed defined
# by "speed" in pixels per second. If speed is 0, camera is teleported to the
# position.
#
# @ESC
extends ESCBaseCommand
class_name CameraSetPosCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
3,
[[TYPE_REAL, TYPE_INT], TYPE_INT, TYPE_INT],
[null, null, null]
)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.set_target(
Vector2(command_params[1], command_params[2]),
command_params[0]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,42 @@
# `camera_set_target speed object`
#
# Configures the camera to set the target to the given `object`using `speed`
# as speed limit.
# This is the default behavior (default follow object is "player").
#
# @ESC
extends ESCBaseCommand
class_name CameraSetTargetCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[[TYPE_REAL, TYPE_INT], TYPE_STRING],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"camera_set_pos: invalid object",
[
"Object with global id %s not found" % arguments[1]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.set_target(
escoria.object_manager.get_object(command_params[1]).node,
command_params[0]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,29 @@
# `camera_set_zoom magnitude [time]`
#
# Zooms the camera in/out to the desired `magnitude`. Values larger than 1 zooms
# the camera out, and smaller values zooms in, relative to the default value
# of 1. An optional `time` in seconds controls how long it takes for the camera
# to zoom into position.
#
# @ESC
extends ESCBaseCommand
class_name CameraSetZoomCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[[TYPE_REAL, TYPE_INT], [TYPE_REAL, TYPE_INT]],
[null, 0.0]
)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.set_camera_zoom(
command_params[0],
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,42 @@
# `camera_set_zoom_height pixels [time]
#
# Zooms the camera in/out to the desired `pixels` height.
# An optional `time` in seconds controls how long it takes for the camera
# to zoom into position.
#
# @ESC
extends ESCBaseCommand
class_name CameraSetZoomHeightCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_INT, [TYPE_INT, TYPE_REAL]],
[null, 0.0]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if arguments[0] < 0:
escoria.logger.report_errors(
"camera_set_zoom_height: invalid height",
[
"Can't zoom to a negative height %d" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.set_camera_zoom(
command_params[0] / escoria.game_size.y,
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,30 @@
# `camera_shift x y [time] [type]`
#
# Shift camera by `x` and `y` pixels over `time` seconds. `type` is any of the
# Tween.TransitionType values without the prefix, eg. LINEAR, QUART or CIRC;
# defaults to QUART.
#
# @ESC
extends ESCBaseCommand
class_name CameraShiftCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_INT, TYPE_INT, [TYPE_INT, TYPE_REAL], TYPE_STRING],
[null, null, 1, "QUAD"]
)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.get_object("camera").node as ESCCamera)\
.shift(
command_params[0],
command_params[1],
command_params[2],
command_params[3]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,132 @@
# `change_scene path run_events`
#
# Loads a new scene, specified by "path". The `run_events` variable is a
# boolean (default true) which you never want to set manually! It's there only
# to benefit save games, so they don't conflict with the scene's events.
#
# @ESC
extends ESCBaseCommand
class_name ChangeSceneCommand
# An array of scenes that have already been loaded
var readied_scenes: Array = []
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING, TYPE_BOOL],
[null, true]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array) -> bool:
if not ResourceLoader.exists(arguments[0]):
escoria.logger.report_errors(
"change_scene: Invalid scene",
["Scene %s was not found" % arguments[0]]
)
return false
if not ResourceLoader.exists(
ProjectSettings.get_setting("escoria/ui/game_scene")
):
escoria.logger.report_errors(
"change_scene: Game scene not found",
[
"The path set in 'ui/game_scene' was not found: %s" % \
ProjectSettings.get_setting("escoria/ui/game_scene")
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.info("Changing scene to %s (run_events = %s)" % [
command_params[0],
command_params[1]
])
if escoria.main.current_scene:
escoria.globals_manager.set_global(
"ESC_LAST_SCENE",
escoria.main.current_scene.global_id,
true
)
escoria.main.scene_transition.fade_out()
yield(escoria.main.scene_transition, "transition_done")
escoria.main_menu_instance.hide()
var res_room = escoria.resource_cache.get_resource(command_params[0])
var res_game = escoria.resource_cache.get_resource(
ProjectSettings.get_setting("escoria/ui/game_scene")
)
# Load game scene
var game_scene = res_game.instance()
if not game_scene:
escoria.logger.report_errors(
"ChangeSceneCommand.run: Failed loading game scene",
[
"Failed loading scene %s" % \
ProjectSettings.get_setting("escoria/ui/game_scene")
]
)
# Load room scene
var room_scene = res_room.instance()
if room_scene:
room_scene.add_child(game_scene)
room_scene.move_child(game_scene, 0)
escoria.main.set_scene(room_scene)
if "esc_script" in room_scene and room_scene.esc_script \
and command_params[1]:
var script = escoria.esc_compiler.load_esc_file(
room_scene.esc_script
)
if script.events.has("setup"):
escoria.event_manager.queue_event(script.events["setup"])
var rc = yield(escoria.event_manager, "event_finished")
while rc[1] != "setup":
rc = yield(escoria.event_manager, "event_finished")
if rc[0] != ESCExecution.RC_OK:
return rc[0]
# If scene was never visited, add "ready" event to the events stack
if not command_params[0] in self.readied_scenes \
and script.events.has("ready"):
escoria.event_manager.queue_event(script.events["ready"])
var rc = yield(escoria.event_manager, "event_finished")
while rc[1] != "ready":
rc = yield(escoria.event_manager, "event_finished")
if rc[0] != ESCExecution.RC_OK:
return rc[0]
escoria.main.scene_transition.fade_in()
yield(escoria.main.scene_transition, "transition_done")
self.readied_scenes.append(command_params[0])
# Clear queued resources
escoria.resource_cache.clear()
escoria.inputs_manager.hotspot_focused = ""
else:
escoria.logger.report_errors(
"ChangeSceneCommand.run: Failed loading room scene",
[
"Failed loading scene %s" % command_params[0]
]
)
return ESCExecution.RC_ERROR
return ESCExecution.RC_OK

View File

@@ -0,0 +1,73 @@
# `custom object node func_name [params]`
#
# Calls the function `func_name` of the node `node` of object `object` with
# the optional `params`. This is a blocking function
#
# @ESC
extends ESCBaseCommand
class_name CustomCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
3,
[TYPE_STRING, TYPE_STRING, TYPE_STRING, TYPE_ARRAY],
[null, null, null, []]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"custom: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
elif not escoria.object_manager.get_object(arguments[0]).node.has_node(
arguments[1]
):
escoria.logger.report_errors(
"custom: invalid node",
[
"Object with global id %s has no node %s" % [
arguments[0],
arguments[1],
]
]
)
return false
elif not escoria.object_manager.get_object(arguments[0]).node\
.get_node(
arguments[1]
)\
.has_method(
arguments[2]
):
escoria.logger.report_errors(
"custom: invalid function",
[
"Object with global id %s and node %s has no function %s" % [
arguments[0],
arguments[1],
arguments[2],
]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
var object = escoria.object_manager.get_object(
command_params[0]
)
object.node.get_node(command_params[1]).call(
command_params[2],
command_params[3]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,49 @@
# `cut_scene object name [reverse]`
#
# Executes the animation specificed with the "name" parameter on the object,
# blocking. The next command in the event will be executed when the animation
# is finished playing. Optional parameters:
#
# * reverse plays the animation in reverse when true
#
# @ESC
extends ESCBaseCommand
class_name CutSceneCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, null, false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"anim: invalid object",
[
"Object with global id %s not found." % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
var obj = escoria.object_manager.objects[command_params[0]]
var anim_id = command_params[1]
var reverse = command_params[2]
var animator = (obj.node as ESCItem).get_animation_player()
if reverse:
animator.play_backwards(anim_id)
else:
animator.play(anim_id)
var animation_finished = yield(animator, "animation_finished")
while animation_finished != anim_id:
animation_finished = yield(animator, "animation_finished")
return ESCExecution.RC_OK

View File

@@ -0,0 +1,22 @@
# `debug string [string2 ...]`
#
# Takes 1 or more strings, prints them to the console.
#
# @ESC
extends ESCBaseCommand
class_name DebugCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING],
[""]
)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.debug("debug command issued", command_params)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,40 @@
# `dec_global name value`
#
# Subtracts the value from global with given "name". Value and global must
# both be integers.
#
# @ESC
extends ESCBaseCommand
class_name DecGlobalCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_INT],
[null, 0]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.globals_manager.get(arguments[0]) is int:
escoria.logger.report_errors(
"dec_global: invalid global",
[
"Global %s didn't have an integer value." % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.globals_manager.set_global(
command_params[0],
escoria.globals_manager.get_global(command_params[0]) - \
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,36 @@
# `enable_terrain node_name`
# Enable the ESCTerrain's NavigationPolygonInstance defined by given node name.
# Disables previously activated NavigationPolygonInstance.
# @ESC
extends ESCBaseCommand
class_name EnableTerrainCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING],
[null]
)
# Run the command
func run(command_params: Array) -> int:
var name : String = command_params[0]
if escoria.room_terrain.has_node(name):
var new_active_navigation_instance = \
escoria.room_terrain.get_node(name)
escoria.room_terrain.current_active_navigation_instance.enabled = false
escoria.room_terrain.current_active_navigation_instance = \
new_active_navigation_instance
escoria.room_terrain.current_active_navigation_instance.enabled = true
return ESCExecution.RC_OK
else:
escoria.logger.report_errors(
"EnableTerrainCommand.run: Can not find terrain node",
[
"Terrain node %s could not be found" % name
]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,29 @@
# `game_over continue_enabled show_credits`
#
# Ends the game. Use the "continue_enabled" parameter to enable or disable the
# continue button in the main menu afterwards. The "show_credits" parameter
# loads the ui/end_credits scene if true. You can configure it to your regular
# credits scene if you want.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name GameOverCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
0,
[TYPE_BOOL, TYPE_BOOL],
[false, true]
)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"game_over: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,48 @@
# `inc_global name value`
#
# Adds the value to global with given "name". Value and global must both be
# integers.
#
# @ESC
extends ESCBaseCommand
class_name IncGlobalCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_INT],
[null, 0]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.globals_manager.has(arguments[0]):
escoria.logger.report_errors(
"inc_global: invalid global",
[
"Global %s does not exist." % arguments[0]
]
)
return false
if not escoria.globals_manager.get(arguments[0]) is int:
escoria.logger.report_errors(
"inc_global: invalid global",
[
"Global %s didn't have an integer value." % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.globals_manager.set_global(
command_params[0],
escoria.globals_manager.get_global(command_params[0]) +\
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,22 @@
# `inventory_add item`
#
# Add an item to the inventory
#
# @ESC
extends ESCBaseCommand
class_name InventoryAddCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING],
[null]
)
# Run the command
func run(command_params: Array) -> int:
escoria.inventory_manager.add_item(command_params[0])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,22 @@
# `inventory_remove item`
#
# Remove an item from the inventory.
#
# @ESC
extends ESCBaseCommand
class_name InventoryRemoveCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING],
[null]
)
# Run the command
func run(command_params: Array) -> int:
escoria.inventory_manager.remove_item(command_params[0])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,28 @@
# `play_snd object file [loop]`
#
# Plays the sound specificed with the "file" parameter on the object, without
# blocking. You can play background sounds, eg. during scene changes, with
# `play_snd bg_snd res://...`
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name PlaySndCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, null, false]
)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"play_snd: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,42 @@
# `queue_animation object animation`
#
# Similar to queue_resource, queues the resources necessary to have an
# animation loaded on an item. The resource paths are taken from the item
# placeholders.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name QueueAnimationCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"queue_animation: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
# TODO: Check if animation is valid
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"queue_animation: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,41 @@
# `queue_resource path [front_of_queue]`
#
# Queues the load of a resource in a background thread. The `path` must be a
# full path inside your game, for example "res://scenes/next_scene.tscn". The
# "front_of_queue" parameter is optional (default value false), to put the
# resource in the front of the queue. Queued resources are cleared when a
# change scene happens (but after the scene is loaded, meaning you can queue
# resources that belong to the next scene).
#
# @ESC
extends ESCBaseCommand
class_name QueueResourceCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[],
[null, false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array) -> bool:
if not ResourceLoader.exists(arguments[0]):
escoria.logger.report_errors(
"queue_resource: Invalid resource",
["Resource %s was not found" % arguments[0]]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.resource_cache.queue_resource(
command_params[0],
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,22 @@
# `repeat`
#
# Restarts the execution of the current scope at the start. A scope can be a
# group or an event.
#
# @ESC
extends ESCBaseCommand
class_name RepeatCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
0,
[],
[]
)
# Run the command
func run(command_params: Array) -> int:
return ESCExecution.RC_CANCEL

View File

@@ -0,0 +1,85 @@
# `say object text [type] [avatar]`
#
# Runs the specified string as a dialog said by the object. Blocks execution
# until the dialog finishes playing. Optional parameters:
#
# * "type" determines the type of dialog UI to use. Default value is "default"
# * "avatar" determines the avatar to use for the dialog. Default value is
# "default"
#
# @ESC
extends ESCBaseCommand
class_name SayCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_STRING, TYPE_STRING],
[
null,
null,
ProjectSettings.get_setting("escoria/ui/default_dialog_scene")\
.get_file().get_basename(),
"default"
]
)
# Run the command
func run(command_params: Array) -> int:
var dict : Dictionary
var dialog_scene_name = ProjectSettings.get_setting(
"escoria/ui/default_dialog_scene"
)
if dialog_scene_name.empty():
escoria.logger.report_errors(
"say()",
[
"Project setting 'escoria/ui/default_dialog_scene' is not set.",
"Please set a default dialog scene."
]
)
return ESCExecution.RC_ERROR
var file = dialog_scene_name.get_file()
var extension = dialog_scene_name.get_extension()
dialog_scene_name = file.rstrip("." + extension)
# Manage specific dialog scene
if command_params.size() > 2:
dialog_scene_name = command_params[2]
# Manage translation/voice lines keys in the form of:
# line_key:"Default line text"
# If a line_key exists, we'll set it a label as it will automatically be
# translated
var dialog_key_line = command_params[1].split(":", true, 1)
if dialog_key_line.size() > 1:
dialog_key_line[1] = dialog_key_line[1].trim_prefix("\"")
dict = {
"key": dialog_key_line[0],
"line": dialog_key_line[1] if dialog_key_line.size() > 1 \
else dialog_key_line[0],
"ui": dialog_scene_name
}
escoria.current_state = escoria.GAME_STATE.DIALOG
if !escoria.dialog_player:
escoria.logger.report_errors(
"No dialog player registered",
[
"No dialog player was registered and the say command was" +
"encountered."
]
)
return ESCExecution.RC_ERROR
escoria.dialog_player.say(command_params[0], dict)
yield(escoria.dialog_player, "dialog_line_finished")
return ESCExecution.RC_OK

View File

@@ -0,0 +1,53 @@
# `sched_event time object event`
#
# Schedules the execution of an "event" found in "object" in a time in seconds.
# If another event is running at the time, execution starts when the running
# event ends.
#
# @ESC
extends ESCBaseCommand
class_name SchedEventCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
3,
[TYPE_INT, TYPE_STRING, TYPE_STRING],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"sched_event: invalid object",
[
"Object with global id %s not found" % arguments[1]
]
)
return false
elif not escoria.object_manager.get_object(arguments[1]).events\
.has(arguments[2]):
escoria.logger.report_errors(
"sched_event: invalid object event",
[
"Object with global id %s has no event %s" % [
arguments[1],
arguments[2],
]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.event_manager.schedule_event(
escoria.object_manager.get_object(command_params[1])\
.events[command_params[2]],
command_params[0]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,37 @@
# `set_active object value`
#
# Changes the "active" state of the object, value can be true or false.
# Inactive objects are hidden in the scene.
#
# @ESC
extends ESCBaseCommand
class_name SetActiveCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_BOOL],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"set_active: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.object_manager.get_object(command_params[0]).active = \
command_params[1]
return ESCExecution.RC_OK

View File

@@ -0,0 +1,45 @@
# `set_angle object degrees`
#
# Turns object to a degrees angle without animations. 0 sets object facing
# forward, 90 sets it 90 degrees clockwise ("east") etc. When turning to the
# destination angle, animations are played if they're defined in animations.
#
# object must be player or interactive. degrees must be between [0, 360] or an
# error is reported.
#
# @ESC
extends ESCBaseCommand
class_name SetAngleCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_INT],
[null, true]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"set_angle: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
# HACK Countering the fact that angle_to_point() function gives
# angle against X axis not Y, we need to check direction using (angle-90°).
# Since the ESC command already gives the right angle, we add 90.
escoria.object_manager.get_object(command_params[0]).node\
.set_angle(int(command_params[1] + 90))
return ESCExecution.RC_OK

View File

@@ -0,0 +1,23 @@
# `set_global name value`
#
# Changes the value of the global "name" with the value. Value can be "true",
# "false" or an integer.
#
# @ESC
extends ESCBaseCommand
class_name SetGlobalCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, [TYPE_INT, TYPE_BOOL, TYPE_STRING]],
[null, null]
)
# Run the command
func run(command_params: Array) -> int:
escoria.globals_manager.set_global(command_params[0], command_params[1])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,27 @@
# `set_globals pattern value`
#
# Changes the value of multiple globals using a wildcard pattern, where "*"
# matches zero or more arbitrary characters and "?" matches any single
# character except a period (".").
#
# @ESC
extends ESCBaseCommand
class_name SetGlobalsCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, [TYPE_BOOL, TYPE_STRING, TYPE_INT]],
[null, null]
)
# Run the command
func run(command_params: Array) -> int:
escoria.globals_manager.set_global_wildcard(
command_params[0],
command_params[1]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,28 @@
# `set_hud_visible visible`
#
# If you have a cutscene like sequence where the player doesn't have control,
# and you also have HUD elements visible, use this to hide the HUD. You want
# to do that because it explicitly signals the player that there is no control
# over the game at the moment. "visible" is true or false.
#
# @ESC
extends ESCBaseCommand
class_name SetHudVisibleCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_BOOL],
[null]
)
# Run the command
func run(command_params: Array) -> int:
if command_params[0]:
escoria.main.current_scene.game.show_ui()
else:
escoria.main.current_scene.game.hide_ui()
return ESCExecution.RC_OK

View File

@@ -0,0 +1,36 @@
# `set_interactive object value`
#
# Sets whether or not an object should be interactive.
#
# @ESC
extends ESCBaseCommand
class_name SetInteractiveCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_BOOL],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"set_interactive: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.object_manager.get_object(command_params[0]).interactive = \
command_params[1]
return ESCExecution.RC_OK

View File

@@ -0,0 +1,50 @@
# `set_sound_state player sound loop`
#
# Change the sound playing on `player` to `sound` with optional looping if
# `loop` is true.
# Valid players are "bg_music" and "bg_sound".
# Aside from paths to sound or music files, the values *off* and *default*.
# *default* is the default value.
# are also valid for `sound`
#
# @ESC
extends ESCBaseCommand
class_name SetSoundStateCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, "default", false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not arguments[0] in ["bg_music", "bg_sound"]:
escoria.logger.report_errors(
"SetSoundStateCommand.validate: invalid player",
[
"Player %s is invalid found" % arguments[0]
]
)
return false
if not arguments[1] in ["default", "off"] \
or not ResourceLoader.exists(arguments[1]):
escoria.logger.report_errors(
"SetSoundStateCommand.validate: invalid sound",
[
"Sound %s is invalid or not found" % arguments[1]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.main.get_node(command_params[0])\
.set_state(command_params[1], command_params[2])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,34 @@
# `set_speed object speed`
#
# Sets how fast object moves. Speed is an integer.
#
# @ESC
extends ESCBaseCommand
class_name SetSpeedCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_INT],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"set_speed: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.objects[command_params[0]].node as ESCItem).\
set_speed(command_params[1])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,41 @@
# `set_state object state [immediate]`
#
# Changes the state of an object, and executes the state animation if present.
# The command can be used to change the appearance of an item or a player
# character.
# If `immediate` is set to true, the animation is run directly
#
# @ESC
extends ESCBaseCommand
class_name SetStateCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_BOOL],
[null, null, false]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"set_state: invalid object",
[
"Object %s not found." % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
(escoria.object_manager.objects[command_params[0]] as ESCObject).set_state(
command_params[1],
command_params[2]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,50 @@
# `slide object1 object2 [speed]`
#
# Moves object1 towards the position of object2, at the speed determined by
# object1's "speed" property, unless overridden. This command is non-blocking.
# It does not respect the room's navigation polygons, so you can move items
# where the player can't walk.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name SlideCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"slide: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"slide: invalid second object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"slide: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,50 @@
# `slide_block object1 object2 [speed]`
#
# Moves object1 towards the position of object2, at the speed determined by
# object1's "speed" property, unless overridden. This command is blocking.
# It does not respect the room's navigation polygons, so you can move items
# where the player can't walk.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name SlideBlockCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"slide_block: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"slide_block: invalid second object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"slide_block: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,61 @@
# `spawn path [object2]`
#
# Instances a scene determined by "path", and places in the position of
# object2 (object2 is optional)
#
# @ESC
extends ESCBaseCommand
class_name SpawnCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_STRING, TYPE_STRING],
[null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not ResourceLoader.exists(arguments[0]):
escoria.logger.report_errors(
"spawn: invalid scene path",
[
"Scene with path %s not found" % arguments[0]
]
)
return false
if arguments[1] and not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"spawn: invalid object",
[
"Object with global id %s not found" % arguments[1]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
var res_scene = escoria.resource_cache.get_resource(command_params[0])
# Load room scene
var scene = res_scene.instance()
if scene:
escoria.main.get_node("/root").add_child(scene)
if command_params[1]:
var obj = escoria.object_manager.get_object(command_params[1])
scene.set_position(obj.get_global_position())
escoria.inputs_manager.hotspot_focused = false
else:
escoria.logger.report_errors(
"spawn: Invalid scene",
[
"Failed loading scene %s" % command_params[0]
]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,21 @@
# `stop`
#
# Stops the event's execution.
#
# @ESC
extends ESCBaseCommand
class_name StopCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
0,
[],
[]
)
# Run the command
func run(command_params: Array) -> int:
return ESCExecution.RC_CANCEL

View File

@@ -0,0 +1,50 @@
# `teleport object1 object2 [angle]`
#
# Sets the position of object1 to the position of object2. By default,
# object2's interact_angle is used to turn object1, but angle will override
# this. Useful for doors and such with an interact_angle you don't always want
# to adhere to when re-entering a room.
#
# @ESC
extends ESCBaseCommand
class_name TeleportCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"teleport: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"teleport: invalid second object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.object_manager.get_object(command_params[0]).node\
.teleport(
escoria.object_manager.get_object(command_params[1]).node,
command_params[2]
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,44 @@
# `turn_to object degrees`
#
# Turns object to a degrees angle with a directions animation.
#
# 0 sets object facing forward, 90 sets it 90 degrees clockwise ("east") etc.
# When turning to the destination angle, animations are played if they're
# defined in animations. object must be player or interactive. degrees must
# be between [0, 360] or an error is reported.
#
# @STUB
# @ESC
extends ESCBaseCommand
class_name TurnToCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_INT],
[null, true]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"turn_to: invalid object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.report_errors(
"turn_to: command not implemented",
[]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,22 @@
# `wait seconds`
#
# Blocks execution of the current script for a number of seconds specified by the "seconds" parameter.
#
# @ESC
extends ESCBaseCommand
class_name WaitCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
1,
[TYPE_INT],
[null]
)
# Run the command
func run(command_params: Array) -> int:
yield(escoria.get_tree().create_timer(command_params[0]), "timeout")
return ESCExecution.RC_OK

View File

@@ -0,0 +1,45 @@
# `walk object1 object2 [speed]`
#
# Walks, using the walk animation, object1 towards the position of object2,
# at the speed determined by object1's "speed" property,
# unless overridden. This command is non-blocking.
#
# @ESC
extends ESCBaseCommand
class_name WalkCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"walk: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"walk: invalid second object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.do("walk", command_params)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,49 @@
# `walk_block object1 object2 [speed]`
#
# Walks, using the walk animation, object1 towards the position of object2,
# at the speed determined by object1's "speed" property,
# unless overridden. This command is blocking.
#
# @ESC
extends ESCBaseCommand
class_name WalkBlockCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
2,
[TYPE_STRING, TYPE_STRING, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"walk_block: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
if not escoria.object_manager.objects.has(arguments[1]):
escoria.logger.report_errors(
"walk_block: invalid second object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.do("walk", command_params)
yield(
(escoria.object_manager.objects[command_params[0]].node as ESCItem),
"arrived"
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,38 @@
# `walk_to_pos player x y`
#
# Makes the `player` walk to the position `x`/`y`.
#
# @ESC
extends ESCBaseCommand
class_name WalkToPosCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
3,
[TYPE_STRING, TYPE_INT, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"walk_to_pos: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.do("walk", [
command_params[0],
Vector2(command_params[1], command_params[2])
])
return ESCExecution.RC_OK

View File

@@ -0,0 +1,42 @@
# `walk_to_pos_block player x y`
#
# Makes the `player` walk to the position `x`/`y`. This is a blocking command.
#
# @ESC
extends ESCBaseCommand
class_name WalkToPosBlockCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
return ESCCommandArgumentDescriptor.new(
3,
[TYPE_STRING, TYPE_INT, TYPE_INT],
[null, null, null]
)
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array):
if not escoria.object_manager.objects.has(arguments[0]):
escoria.logger.report_errors(
"walk_to_pos_block: invalid first object",
[
"Object with global id %s not found" % arguments[0]
]
)
return false
return .validate(arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.do("walk", [
command_params[0],
Vector2(command_params[1], command_params[2])
])
yield(
(escoria.object_manager.objects[command_params[0]].node as ESCItem),
"arrived"
)
return ESCExecution.RC_OK

View File

@@ -0,0 +1,188 @@
# Manages currently carried out actions
extends Object
class_name ESCActionManager
# The current action was changed
signal action_changed
# Current verb used
var current_action : String = "" setget set_current_action
# Current tool (ESCItem/ESCInventoryItem) used
var current_tool: ESCObject
# Set the current action
func set_current_action(action : String):
if action != current_action:
clear_current_tool()
current_action = action
emit_signal("action_changed")
# Clear the current action
func clear_current_action():
set_current_action("")
# Clear the current tool
func clear_current_tool():
current_tool = null
# Activates the action for given params
#
# #### Parameters
#
# - action String Action to execute (defined in attached ESC file and in
# action verbs UI) eg: arrived, use, look, pickup...
# - target: Target ESC object
# - combine_with: ESC object to combine with
func activate(
action: String,
target: ESCObject,
combine_with: ESCObject = null
) -> int:
escoria.logger.info("Activated action %s on %s" % [action, target])
# If we're using an action which item requires to combine
if target.node is ESCItem\
and action in target.node.combine_if_action_used_among:
# Check if object must be in inventory to be used
if target.node.use_from_inventory_only:
if escoria.inventory_manager.inventory_has(target.global_id):
# Player has item in inventory, we check the element to use on
if combine_with:
var do_combine = true
if combine_with.node is ESCItem \
and combine_with.node.use_from_inventory_only\
and not escoria.inventory_manager.inventory_has(
combine_with.global_id
):
do_combine = false
if do_combine:
var target_event = "%s %s" % [
action,
combine_with.global_id
]
var combine_with_event = "%s %s" % [
action,
target.global_id
]
if target.events.has(target_event):
escoria.event_manager.queue_event(target.events[
target_event
])
var event_returned = yield(
escoria.event_manager,
"event_finished"
)
while event_returned[1] != target_event:
event_returned = yield(
escoria.event_manager,
"event_finished"
)
if event_returned[0] == ESCExecution.RC_OK:
escoria.action_manager\
.clear_current_action()
return event_returned[0]
elif combine_with.events.has(combine_with_event)\
and not combine_with.node.combine_is_one_way:
escoria.event_manager.queue_event(
combine_with.events[
combine_with_event
]
)
var event_returned = yield(
escoria.event_manager,
"event_finished"
)
while event_returned[1] != combine_with_event:
event_returned = yield(
escoria.event_manager,
"event_finished"
)
if event_returned[0] == ESCExecution.RC_OK:
escoria.action_manager\
.clear_current_action()
return event_returned[0]
else:
var errors = [
"Attempted to execute action %s between item %s and item %s" % [
action,
target.global_id,
combine_with.global_id
]
]
if combine_with.node.combine_is_one_way:
errors.append(
"Reason: %s's item interaction " +\
"is one-way." % combine_with.global_id
)
escoria.logger.report_warnings(
"ESCActionManager.activate: Invalid action",
errors
)
return ESCExecution.RC_ERROR
else:
escoria.logger.report_warnings(
"ESCActionManager.activate: Invalid action on item",
[
"Trying to combine object %s with %s, "+
"but %s is not in inventory." % [
target.global_id,
combine_with.global_id,
combine_with.global_id
]
]
)
return ESCExecution.RC_ERROR
else:
# We're missing a target here.
# Tell the Label to add a conjunction and wait for another
# click to add the target to p_param. Until then, return
current_tool = target
return ESCExecution.RC_OK
else:
escoria.logger.report_warnings(
"ESCActionManager.activate: Invalid action on item",
[
"Trying to run %s on object %s, "+
"but item must be in inventory." % [
action,
target.global_id
]
]
)
if target.events.has(action):
escoria.event_manager.queue_event(target.events[action])
var event_returned = yield(
escoria.event_manager,
"event_finished"
)
while event_returned[1] != action:
event_returned = yield(
escoria.event_manager,
"event_finished"
)
if event_returned[0] == ESCExecution.RC_OK:
escoria.action_manager.clear_current_action()
return event_returned[0]
else:
escoria.logger.report_warnings(
"ESCActionManager.activate: Invalid action",
[
"Event for action %s on object %s not found." % [
action,
target.global_id
]
]
)
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,47 @@
# A registry of ESC command objects
extends Object
class_name ESCCommandRegistry
# The registry of registered commands
var registry: Dictionary = {}
# Load a command by its name
#
# #### Parameters
#
# - command_name: Name of command to load
# **Returns** The command object
func load_command(command_name: String) -> ESCBaseCommand:
for command_directory in ProjectSettings.get(
"escoria/main/command_directories"
):
if ResourceLoader.exists("%s/%s.gd" % [command_directory, command_name]):
registry[command_name] = load(
"%s/%s.gd" % [command_directory, command_name]
).new()
return registry[command_name]
escoria.logger.report_errors(
"ESCCommandRegistry.load_command: Command not found",
[
"No command class could be found for command %s" %
command_name
]
)
return null
# Retrieve a command from the command registry
#
# #### Parameters
#
# - command_name: The name of the command
# **Returns** The command object
func get_command(command_name: String) -> ESCBaseCommand:
if self.registry.has(command_name):
return self.registry[command_name]
else:
return self.load_command(command_name)

View File

@@ -1,545 +1,220 @@
extends Node # Compiler of the ESC language
""" extends Object
ESC files: class_name ESCCompiler
Lines beginning with ":" such as :push, :say are EVENTS.
Lines in between are usually the ESC API functions calls. They are called COMMANDS.
Steps # A RegEx for comment lines
compile_script(path/to/esc) : called once const COMMENT_REGEX = '^\\s*#.*$'
> compile(path/to/esc, errors) : called once
> read_events() : called once # A RegEx for empty lines
> create an ESCState, initialized with 1st line const EMPTY_REGEX = '^\\s*$'
> for each line in ESCState that corresponds to an event (:event), create a new level
> add_level(state, level, errors) # A RegEx for finding out the indent of a line
> for each state.line that belongs to same group (same indentation), create a command const INDENT_REGEX = '^(?<indent>\\s*)'
> read_cmd(state, level, errors)
> get the token in state.line : this is the actual command (say, teleport, etc.)
> get the parameters next to the token
> create an ESCCommand, check it and push it into level array
> create an ESCEvent with the level created
> add it to the returned Dictionary of events
In the end, the ESCState has read all lines in the file and is deleted
Returned value is a Dictionary { event name : ESCEvent}
And ESCEvent.level is an array of ESCCommand
"""
# The currently compiled event
var _current_event = null
var commands = { # A stack of groups currently compiling
"accept_input": { "min_args": 1, "types": [TYPE_STRING] }, var _groups_stack = []
"autosave": { "min_args": 0 },
"anim": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL, TYPE_BOOL, TYPE_BOOL] },
"camera_push": { "min_args": 1, "types": [TYPE_STRING] },
"camera_set_drag_margin_enabled": { "min_args": 2, "types": [TYPE_BOOL, TYPE_BOOL] },
"camera_set_limits": { "min_args": 1, "types": [TYPE_INT]},
"camera_set_pos": { "min_args": 3, "types": [TYPE_REAL, TYPE_INT, TYPE_INT] },
"camera_set_target": { "min_args": 1, "types": [TYPE_REAL] },
"camera_set_zoom": { "min_args": 1, "types": [TYPE_REAL] },
"camera_set_zoom_height": { "min_args": 1, "types": [TYPE_INT] },
"camera_shift": { "min_args": 2, "types": [TYPE_INT, TYPE_INT] },
"change_scene": { "min_args": 1, "types": [TYPE_STRING, TYPE_BOOL] },
"custom": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING] },
"cut_scene": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL, TYPE_BOOL, TYPE_BOOL] },
"debug": { "min_args": 1 },
"dec_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] },
# "dialog_config": { "min_args": 3, "types": [TYPE_STRING, TYPE_BOOL, TYPE_BOOL] },
"enable_terrain": { "min_args": 1, "types": [TYPE_STRING]},
"game_over": { "min_args": 1, "types": [TYPE_BOOL] },
"inc_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] },
"inventory_add": { "min_args": 1 },
"inventory_remove": { "min_args": 1 },
"inventory_display": { "min_args": 1, "types": [TYPE_BOOL] },
"jump": { "min_args": 1 },
"label": { "min_args": 1 },
"set_sound_state": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL] },
"queue_animation": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL] },
"queue_resource": { "min_args": 1, "types": [TYPE_STRING, TYPE_BOOL] },
"repeat": true,
"set_state": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL] },
"set_hud_visible": { "min_args": 1, "types": [TYPE_BOOL]},
"say": { "min_args": 2 },
"sched_event": { "min_args": 3, "types": [TYPE_REAL, TYPE_STRING, TYPE_STRING] },
"set_active": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] },
"set_angle": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] },
"set_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING] },
"set_globals": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] },
"set_interactive": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] },
"set_speed": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] },
"slide": { "min_args": 2 },
"slide_block": { "min_args": 2 },
"spawn": { "min_args": 1 },
"stop": true,
"superpose_scene": { "min_args": 1, "types": [TYPE_STRING, TYPE_BOOL] },
"teleport": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_INT] },
"teleport_pos": { "min_args": 3 },
"turn_to": { "min_args": 2 },
"wait": true,
"walk": { "min_args": 2 },
"walk_block": { "min_args": 2 },
"walk_to_pos": { "min_args": 3},
"walk_to_pos_block": { "min_args": 3},
"%": { "alias": "label", "min_args": 1},
"?": { "alias": "dialog"},
"!": { "alias": "end_dialog", "min_args": 0 },
">": { "alias": "branch"},
}
# Commands that can be called only by the ESC debug prompt # A stack of dialogs currently compiling
var debug_commands = { var _dialogs_stack = []
"get_active": { "min_args": 1, "types": [TYPE_STRING] },
"get_global": { "min_args": 1, "types": [TYPE_STRING] },
"get_interactive": { "min_args": 1, "types": [TYPE_STRING] },
"get_state": { "min_args": 1, "types": [TYPE_STRING] },
}
# Loads a Dictionary of actions from a file, given its path. # A stack of dialog options currently compiling
func load_esc_file(esc_file_path : String) -> Dictionary: var _dialogs_option_stack = []
var f = File.new()
if !f.file_exists(esc_file_path):
escoria.logger.report_errors("esc_compiler.gd:load_esc_file()", ["File " + esc_file_path + " not found."])
return {}
return compile_script(esc_file_path)
# Loads the parameter script file. Can be either GDScript of ESC type. # A pointer to the current container (group, dialog option)
# Returns the Dictionary of actions loaded from the file. # that should get the current command
func compile_script(p_path : String) -> Dictionary: var _command_container = []
var ev_table
# Script is GDScript
if p_path.find(".gd") != -1:
var res = ResourceLoader.load(p_path)
if res == null:
return {}
ev_table = res.new().get_events()
else: # Script is ESC
var errors = []
ev_table = compile(p_path, errors)
if errors.size() > 0:
escoria.logger.call_deferred("report_errors", p_path, errors)
return ev_table
func check_command(commands_list : Dictionary, cmd : esctypes.ESCCommand, state : esctypes.ESCState, errors : Array):
if !(cmd.name in commands_list):
errors.push_back("line "+str(state.line_count)+": command "+cmd.name+" not valid.")
return false
var cmd_data = commands_list[cmd.name] # The currently identified indent
if typeof(cmd_data) == TYPE_BOOL: var _current_indent = 0
return true
if "alias" in cmd_data:
cmd.name = cmd_data.alias
if "min_args" in cmd_data: func _init():
if cmd.params.size() < cmd_data.min_args: # Assure command list preference
errors.push_back("line "+str(state.line_count)+": command "+cmd.name+" takes "+str(cmd_data.min_args)+" parameters ("+str(cmd.params.size())+" were given).") if not ProjectSettings.has_setting("escoria/esc/command_paths"):
return false ProjectSettings.set_setting("escoria/esc/command_paths", [
"res://addons/escoria-core/game/core-scripts/esc/commands"
var ret = true ])
if "types" in cmd_data: var property_info = {
var i = 0 "name": "escoria/esc/command_paths",
for t in cmd_data.types: "type": TYPE_STRING_ARRAY
if i >= cmd.params.size():
break
if t == TYPE_BOOL:
if cmd.params[i] == "true":
cmd.params[i] = true
elif cmd.params[i] == "false":
cmd.params[i] = false
else:
errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Must be 'true' or 'false'.")
ret = false
if t == TYPE_INT:
if not cmd.params[i].is_valid_integer():
errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Expected integer.")
cmd.params[i] = int(cmd.params[i])
if t == TYPE_REAL:
if not cmd.params[i].is_valid_float():
errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Expected float.")
cmd.params[i] = float(cmd.params[i])
i+=1
return ret
# Check that the given command exists and respects the right number of parameters
func check_normal_command(cmd : esctypes.ESCCommand, state : esctypes.ESCState, errors : Array):
return check_command(commands, cmd, state, errors)
func check_debug_command(cmd : esctypes.ESCCommand, state : esctypes.ESCState, errors : Array):
return check_command(debug_commands, cmd, state, errors)
# Fills the given "state" with the next line read from the file
func read_line(state : esctypes.ESCState) -> void:
while true:
if _eof_reached(state.file):
state.line = null
return
else:
state.line = _get_line(state.file)
state.line_count += 1
if !is_comment(state.line):
return
# Returns true if line is a comment (starting with #)
func is_comment(line : String) -> bool:
for i in range(0, line.length()):
var c = line[i]
if c == "#":
return true
if c != " " && c != "\t":
return false
return true
# Returns the position of the first non-blank character in given line string
func get_indent(line : String):
for i in range(0, line.length()):
if line[i] != " " && line[i] != "\t":
return i
# If the given line string is a event (begins with ":"), returns its name
# Else, return false
func is_event(line : String):
var trimmed = trim(line)
if trimmed.find(":") == 0:
return trimmed.substr(1, trimmed.length()-1)
return false
# Returns true if the given string is a flag (ie. "[.+]")
func is_flags(tk : String) -> bool:
var trimmed = trim(tk)
if trimmed.find("[") == 0 && trimmed.find("]") == trimmed.length()-1:
return true
return false
# Reads each line contained in the state (ESCState) (updates state.line)
# While the new line belongs to the same group, creates an ESCCommand from the current state
func add_level(state : esctypes.ESCState, level : Array, errors : Array):
read_line(state)
while state.line != null:
if is_event(state.line):
return
var ind_level = get_indent(state.line)
if ind_level < state.indent:
return
if ind_level > state.indent:
errors.push_back("line "+str(state.line_count)+": invalid indentation for group")
read_line(state)
continue
read_cmd(state, level, errors)
func add_dialog(state : esctypes.ESCState, level : Array, errors : Array):
read_line(state)
while typeof(state.line) != typeof(null):
if is_event(state.line):
return
var ind_level = get_indent(state.line)
if ind_level < state.indent:
return
if ind_level > state.indent:
errors.push_back("line "+str(state.line_count)+": invalid indentation for dialog")
read_line(state)
continue
read_dialog_option(state, level, errors)
func get_token(line : String, p_from : int, line_count : int, errors : Array) -> int:
while p_from < line.length():
if line[p_from] == " " || line[p_from] == "\t":
p_from += 1
else:
break
if p_from >= line.length():
return -1
var tk_end
if line[p_from] == "[":
tk_end = line.find("]", p_from)
if tk_end == -1:
errors.push_back("line "+str(line_count)+": unterminated flags")
tk_end += 1
elif line[p_from] == "\"":
tk_end = line.find("\"", p_from+1)
if tk_end == -1:
errors.push_back("line "+str(line_count)+": unterminated quotes, line '"+line+"'")
else:
tk_end = p_from
while tk_end < line.length():
if line[tk_end] == ":":
var ntk = get_token(line, tk_end+1, line_count, errors)
tk_end = ntk
break
if line[tk_end] == " " || line[tk_end] == "\t":
break
tk_end += 1
return tk_end
# Remove blank characters around p_str
func trim(p_str : String) -> String:
while p_str.length() && (p_str[0] == " " || p_str[0] == "\t"):
p_str = p_str.substr(1, p_str.length()-1)
while p_str.length() && p_str[p_str.length()-1] == " " || p_str[p_str.length()-1] == "\t":
p_str = p_str.substr(0, p_str.length()-1)
if p_str[0] == "\"":
p_str = p_str.substr(1, p_str.length()-1)
if p_str[p_str.length()-1] == "\"":
p_str = p_str.substr(0, p_str.length()-1)
return p_str
# Parses a flags string (usually defined by '[.*]') and fills the flags_list array
# and ifs variable (Dictionary containing all ifs conditions)
func parse_flags(p_flags : String, flags_list : Array, ifs : Dictionary):
var from = 1
while true:
var next = p_flags.find(",", from)
var flag
if next == -1:
flag = p_flags.substr(from, (p_flags.length()-1) - from)
else:
flag = p_flags.substr(from, next - from)
flag = trim(flag)
var list = []
if flag[0] == "!":
list.push_back(true)
flag = trim(flag.substr(1, flag.length()-1))
if flag.find("inv-") == 0:
ifs["if_not_inv"].push_back(trim(flag).substr(4, flag.length()-1))
elif flag.find("a/") == 0:
ifs["if_not_active"].push_back(trim(flag).substr(2, flag.length() - 1))
elif flag.substr(0, 3) in ["eq ", "gt ", "lt "]:
var elems = flag.split(" ", true, 2)
var comparison = "ne" if elems[0] == "eq" else "le" if elems[0] == "gt" else "ge"
ifs["if_" + comparison].push_back([elems[1], elems[2]])
else:
ifs["if_false"].push_back(trim(flag))
else:
list.push_back(false)
if flag.find("inv-") == 0:
ifs["if_inv"].push_back(trim(flag).substr(4, flag.length()-1))
elif flag.find("a/") == 0:
ifs["if_active"].push_back(trim(flag).substr(2, flag.length() - 1))
elif flag.substr(0, 3) in ["eq ", "gt ", "lt "]:
var elems = flag.split(" ", true, 2)
ifs["if_" + elems[0]].push_back([elems[1], elems[2]])
else:
ifs["if_true"].push_back(trim(flag))
if flag.find(":") >= 0:
var pos = flag.substr(0, flag.find(":"))
var inv = flag.substr(0, pos)
inv = trim(inv)
list.push_back(inv)
flag = flag.substr(pos, flag.length() - pos)
elif flag.find("inv-") == 0:
flag = trim(flag).substr(4, flag.length()-1)
list.push_back("i")
else:
list.push_back("g")
list.push_back(trim(flag))
# printt("adding flag ", list)
flags_list.push_back(list)
if next == -1:
return
from = next+1
func read_dialog_option(state : esctypes.ESCState, level : Array, errors : Array):
var tk_end = get_token(state.line, 0, state.line_count, errors)
var tk = trim(state.line.substr(0, tk_end))
if tk != "*" && tk != "-":
errors.append("line "+str(state.line_count)+": Invalid dialog option")
read_line(state)
return
# Remove inline comments
var comment_idx = state.line.find("#")
if comment_idx > -1:
state.line = state.line.substr(0, comment_idx)
tk_end += 1
# var c_start = state.line.find("\"", 0)
var c_end = state.line.find_last("\"")
var q_end = state.line.find("[", c_end)
var q_flags = null
#printt("flags before", q_flags)
if q_end == -1:
q_end = state.line.length()
else:
var f_end = state.line.find("]", q_end)
if f_end == -1:
errors.append("line "+str(state.line_count)+": unterminated flags")
else:
f_end += 1
q_flags = state.line.substr(q_end, f_end - q_end)
var question = trim(state.line.substr(tk_end, q_end - tk_end))
var cmd = { "name": "*", "params": [question, []] }
if q_flags:
var ifs = {
"if_true": [], "if_false": [], "if_inv": [], "if_not_inv": [],
"if_active": [], "if_not_active": [],
"if_eq": [], "if_ne": [], # string and integer comparison
"if_gt": [], "if_ge": [], "if_lt": [], "if_le": [] # integer comparison
} }
var flag_list = [] ProjectSettings.add_property_info(property_info)
parse_flags(q_flags, flag_list, ifs)
for key in ifs:
if ifs[key].size():
cmd.conditions[key] = ifs[key]
if flag_list.size():
cmd.flags = flag_list
state.indent += 1
add_level(state, cmd.params[1], errors)
state.indent -= 1
level.push_back(cmd)
# Read an ESCState and converts it to ESCCommand
# then puts it into level (Array of ESCCommand)
func read_cmd(state : esctypes.ESCState, level : Array, errors : Array):
var params = []
var from = 0
var tk_end = get_token(state.line, from, state.line_count, errors)
var ifs = {
"if_true": [], "if_false": [], "if_inv": [], "if_not_inv": [],
"if_active": [], "if_not_active": [],
"if_eq": [], "if_ne": [], # string and integer comparison
"if_gt": [], "if_ge": [], "if_lt": [], "if_le": [] # integer comparison
}
var flags = []
while tk_end != -1:
var tk = trim(state.line.substr(from, tk_end - from))
from = tk_end + 1
if is_flags(tk):
parse_flags(tk, flags, ifs)
else:
params.push_back(tk)
tk_end = get_token(state.line, from, state.line_count, errors)
if params.size() == 0:
errors.append("line "+str(state.line_count)+": Invalid command.")
read_line(state)
return
var cmd = esctypes.ESCCommand.new(params[0])
if params[0] == ">":
cmd.params = []
state.indent += 1
add_level(state, cmd.params, errors)
state.indent -= 1
elif params[0] == "?":
params.remove(0)
var dialog_params = []
state.indent += 1
add_dialog(state, dialog_params, errors)
cmd.params = params
cmd.params.insert(0, dialog_params)
state.indent -= 1
elif params[0] == "*":
errors.push_back("line "+str(state.line_count)+": Invalid command: dialog option outside dialog")
read_line(state)
return
else:
params.remove(0)
# Remove inline comments
var comment_idx = params.find("#")
if comment_idx > -1:
params.resize(comment_idx)
cmd.params = params
read_line(state)
for key in ifs:
if ifs[key].size():
cmd.conditions[key] = ifs[key]
if flags.size():
cmd.flags = flags
var errors_before = errors.duplicate()
var valid = check_normal_command(cmd, state, errors) # Load an ESC file from a file resource
if valid: func load_esc_file(path: String) -> ESCScript:
level.push_back(cmd) escoria.logger.debug("Parsing file %s" % path)
if File.new().file_exists(path):
var file = File.new()
file.open(path, File.READ)
var lines = []
while not file.eof_reached():
lines.append(file.get_line())
return self.compile(lines)
else: else:
var debug_valid = check_debug_command(cmd, state, errors) escoria.logger.report_errors(
if debug_valid: "Can not find ESC file",
errors.clear() [
level.push_back(cmd) "File %s could not be found" % path
]
)
return null
# Read events from f (Dictionary or File) into ret Dictionary
func read_events(f, ret : Dictionary, errors : Array):
#var state = { "file": f, "line": _get_line(f), "indent": 0, "line_count": 0 }
var state = esctypes.ESCState.new(f, _get_line(f), 0, 0)
while state.line != null: # Compiles an array of ESC script strings to an ESCScript
if is_comment(state.line): func compile(lines: Array) -> ESCScript:
read_line(state) var script = ESCScript.new()
continue if lines.size() > 0:
var ev = is_event(state.line) var events = self._compile(lines)
if typeof(ev) != typeof(null): for event in events:
var level = [] script.events[event.name] = event
var abort = add_level(state, level, errors)
var ev_flags = [] return script
if ev is String:
if "|" in ev:
var ev_split = ev.split("|", true, 1) # Compile an array of ESC script lines into an array of ESC objects
ev = ev_split[0] func _compile(lines: Array) -> Array:
ev = ev.strip_edges()
if ev_split.size() > 1: var comment_regex = RegEx.new()
ev_split[1] = ev_split[1].strip_edges() comment_regex.compile(COMMENT_REGEX)
ev_flags = ev_split[1].split(" ") var empty_regex = RegEx.new()
empty_regex.compile(EMPTY_REGEX)
var indent_regex = RegEx.new()
indent_regex.compile(INDENT_REGEX)
ret[ev] = esctypes.ESCEvent.new(ev, level, Array(ev_flags)) var event_regex = RegEx.new()
if abort: event_regex.compile(ESCEvent.REGEX)
return abort var command_regex = RegEx.new()
command_regex.compile(ESCCommand.REGEX)
# If f is a File, returns the next line as String (or null) var dialog_regex = RegEx.new()
# If f is a Dictionary, returns the next line from f.lines dialog_regex.compile(ESCDialog.REGEX)
func _get_line(f): var dialog_end_regex = RegEx.new()
if f is Dictionary: dialog_end_regex.compile(ESCDialog.END_REGEX)
if f.line >= f.lines.size(): var dialog_option_regex = RegEx.new()
return null dialog_option_regex.compile(ESCDialogOption.REGEX)
var line = f.lines[f.line] var group_regex = RegEx.new()
f.line += 1 group_regex.compile(ESCGroup.REGEX)
#printt("reading line ", line)
return line var returned = []
else:
return f.get_line() while lines.size() > 0:
var line = lines.pop_front()
func _eof_reached(f): escoria.logger.debug("Parsing line %s" % line)
if typeof(f) == typeof({}): if comment_regex.search(line) or empty_regex.search(line):
return f.line >= f.lines.size() # Ignore comments and empty lines
else: escoria.logger.debug("Line is empty or a comment. Skipping.")
return f.eof_reached() continue
var indent = \
func compile_str(p_str : String, errors : Array): escoria.utils.get_re_group(
var f = { "line": 0, "lines": p_str.split("\n") } indent_regex.search(line),
"indent"
#printt("esc compile str ", f) ).length()
var ret = {} if event_regex.search(line):
read_events(f, ret, errors) var event = ESCEvent.new(line)
escoria.logger.debug("Line is the event %s" % event.name)
#printt("returning ", p_fname, ret) var event_lines = []
return ret while lines.size() > 0:
var next_line = lines.pop_front()
# Returns a Dictionary of events read from p_fname filename if not event_regex.search(next_line):
func compile(p_fname : String, errors : Array) -> Dictionary: event_lines.append(next_line)
var f = File.new() else:
f.open(p_fname, File.READ) lines.push_front(next_line)
if !f.is_open(): break
return {} if event_lines.size() > 0:
escoria.logger.debug(
var ret = {} "Compiling the next %d lines into the event" % \
read_events(f, ret, errors) event_lines.size()
)
#printt("returning ", p_fname, ret) event.statements = self._compile(event_lines)
return ret returned.append(event)
elif group_regex.search(line):
var group = ESCGroup.new(line)
escoria.logger.debug("Line is a group")
var group_lines = []
while lines.size() > 0:
var next_line = lines.pop_front()
var next_line_indent = \
escoria.utils.get_re_group(
indent_regex.search(next_line),
"indent"
).length()
if next_line_indent > indent:
group_lines.append(next_line)
else:
lines.push_front(next_line)
break
if group_lines.size() > 0:
escoria.logger.debug(
"Compiling the next %d lines into the group" % \
group_lines.size()
)
group.statements = self._compile(group_lines)
returned.append(group)
elif dialog_regex.search(line):
var dialog = ESCDialog.new(line)
escoria.logger.debug("Line is a dialog")
var dialog_lines = []
while lines.size() > 0:
var next_line = lines.pop_front()
var end_line = dialog_end_regex.search(next_line)
if end_line and \
escoria.utils.get_re_group(
end_line,
"indent"
).length() == indent:
break
else:
dialog_lines.append(next_line)
if dialog_lines.size() > 0:
escoria.logger.debug(
"Compiling the next %d lines into the dialog" % \
dialog_lines.size()
)
dialog.options = self._compile(dialog_lines)
# Remove the end line from the stack
lines.pop_front()
returned.append(dialog)
elif dialog_option_regex.search(line):
var dialog_option = ESCDialogOption.new(line)
escoria.logger.debug(
"Line is the dialog option %s" % \
dialog_option.option
)
var dialog_option_lines = []
while lines.size() > 0:
var next_line = lines.pop_front()
var next_line_indent = \
escoria.utils.get_re_group(
indent_regex.search(next_line),
"indent"
).length()
if next_line_indent > indent:
dialog_option_lines.append(next_line)
else:
lines.push_front(next_line)
break
if dialog_option_lines.size() > 0:
escoria.logger.debug(
"Compiling the next %d lines into the event" % \
dialog_option_lines.size()
)
dialog_option.statements = self._compile(dialog_option_lines)
returned.append(dialog_option)
elif command_regex.search(line):
var command = ESCCommand.new(line)
escoria.logger.debug("Line is the command %s" % command.name)
returned.append(command)
else:
escoria.logger.report_errors(
"Invalid ESC line detected",
[
"Line couldn't be compiled: %s" % line
]
)
return returned

View File

@@ -0,0 +1,60 @@
# A manager for running events
extends Node
class_name ESCEventManager
# Emitted when the event did finish running
signal event_finished(event_name, return_code)
# A queue of events to run
var events_queue: Array = []
# A list of currently scheduled events
var scheduled_events: Array = []
# Handle the events queue and scheduled events
func _process(delta: float) -> void:
if events_queue.size() > 0:
var running_event = events_queue.pop_front()
# TODO: Handle event flags
if not running_event.is_connected(
"finished", self, "_on_event_finished"
):
running_event.connect(
"finished",
self,
"_on_event_finished",
[running_event]
)
running_event.run()
for event in self.scheduled_events:
(event as ESCScheduledEvent).timeout -= delta
if (event as ESCScheduledEvent).timeout <= 0:
self.scheduled_events.erase(event)
self.events_queue.append(event)
# Queue a new event to run
func queue_event(event: ESCEvent) -> void:
events_queue.append(event)
# Schedule an event to run after a timeout
func schedule_event(event: ESCEvent, timeout: float) -> void:
scheduled_events.append(ESCScheduledEvent.new(event, timeout))
# The event finished running
func _on_event_finished(return_code: int, event: ESCEvent) -> void:
escoria.logger.debug(
"Event %s ended with return code %d" % [event.name, return_code]
)
event.disconnect("finished", self, "_on_event_finished")
match(return_code):
ESCExecution.RC_CANCEL:
self.scheduled_events = []
self.events_queue = []
return_code = ESCExecution.RC_OK
emit_signal("event_finished", return_code, event.name)

View File

@@ -0,0 +1,90 @@
# A resource that manages the ESC global states
# The ESC global state is basically simply a dictionary of keys with
# values. Values can be bool, integer or strings
extends Resource
class_name ESCGlobalsManager
# Emitted when a global is changed
signal global_changed(global, old_value, new_value)
# A list of reserved globals which can not be overridden
const RESERVED_GLOBALS = [
"ESC_LAST_SCENE"
]
# The globals registry
export(Dictionary) var _globals = {}
# Check if a global was registered
#
# #### Parameters
#
# - key: The global key to check
# **Returns** Wether the global was registered
func has(key: String) -> bool:
return _globals.has(key)
# Get the current value of a global
#
# #### Parameters
#
# - key: The key of the global to return the value
# **Returns** The value of the global
func get_global(key: String):
if _globals.has(key):
return _globals[key]
return null
# Filter the globals and return all matching keys and their values as
# a dictionary
# Check out [the Godot docs](https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-match)
# for the pattern format
#
# #### Parameters
#
# - pattern: The pattern that the keys have to match
# **Returns** A dictionary of matching keys and their values
func filter(pattern: String) -> Dictionary:
var ret = {}
for global_key in _globals.keys():
if global_key.match(pattern):
ret[global_key] = _globals[global_key]
return ret
# Set the value of a global
#
# #### Parameters
#
# - key: The key of the global to modify
# - value: The new value
func set_global(key: String, value, ignore_reserved: bool = false) -> void:
if key in RESERVED_GLOBALS and not ignore_reserved:
escoria.logger.report_errors(
"ESCGlobalsManager.set_global: Can not override reserved global",
[
"Global key %s is reserved and can not be overridden" % key
]
)
_globals[key] = value
emit_signal("global_changed", key, _globals[key], value)
# Set all globals that match the pattern to the value
# Check out [the Godot docs](https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-match)
# for the pattern format
#
# #### Parameters
#
# - pattern: The wildcard pattern to match
# - value: The new value
func set_global_wildcard(pattern: String, value) -> void:
for global_key in _globals.keys:
if global_key.match(pattern):
self.set_global(global_key, value)

View File

@@ -0,0 +1,50 @@
# A manager for inventory objects
extends Object
class_name ESCInventoryManager
# Check if the player has an inventory item
#
# #### Parameters
#
# - item: Inventory item id
# **Returns** Wether the player has the inventory
func inventory_has(item: String) -> bool:
return escoria.globals_manager.has("i/%s" % item)
# Get all inventory items
# **Returns** The items in the inventory
func items_in_inventory() -> Array:
var items = []
var filtered = escoria.globals_manager.filter("i/*")
for glob in filtered.keys():
if filtered[glob]:
items.append(glob.rsplit("i/", false)[0])
return items
# Remove an item from the inventory
#
# #### Parameters
#
# - item: Inventory item id
func remove_item(item: String):
if not inventory_has(item):
self.logger.report_errors(
"ESCInventoryManager.remove_item: Error removing inventory item",
[
"Trying to remove non-existent item %s" % item
]
)
else:
escoria.globals_manager.set_global("i/%s" % item, false)
# Add an item to the inventory
#
# #### Parameters
#
# - item: Inventory item id
func add_item(item: String):
escoria.globals_manager.set_global("i/%s" % item, true)

View File

@@ -0,0 +1,95 @@
# A manager for ESC objects
extends Node
class_name ESCObjectManager
const RESERVED_OBJECTS = [
"bg_music"
]
# The hash of registered objects (the global id is the key)
var objects: Dictionary = {}
# Make active objects visible
func _process(_delta):
for object in objects:
if (object as ESCObject).node:
(object as ESCObject).node.visible = (object as ESCObject).active
# Register the object in the manager
#
# #### Parameters
#
# - object: Obejct to register
# - force: Register the object, even if it has already been registered
func register_object(object: ESCObject, force: bool = false) -> void:
if objects.has(object.global_id) and not force:
escoria.logger.report_errors(
"ESCObjectManager.register_object: Object already registered",
[
"Object with global id %s already registered" %
object.global_id
]
)
else:
if not object.node.is_connected(
"tree_exited",
self,
"unregister_object"
):
object.node.connect(
"tree_exited",
self,
"unregister_object",
[object]
)
if "is_interactive" in object.node and object.node.is_interactive:
object.interactive = true
if "esc_script" in object.node and not object.node.esc_script.empty():
var script = escoria.esc_compiler.load_esc_file(
object.node.esc_script
)
object.events = script.events
objects[object.global_id] = object
# Check wether an object was registered
#
# #### Parameters
#
# - global_id: Global ID of object
# **Returns** Wether the object exists in the object registry
func has(global_id: String) -> bool:
return objects.has(global_id)
# Get the object from the object registry
func get_object(global_id: String) -> ESCObject:
if objects.has(global_id):
return objects[global_id]
else:
escoria.logger.report_warnings(
"Invalid object retrieved",
[
"Object with global id %s not found" % global_id
]
)
return null
# Remove an object from the registry
#
# #### Parameters
#
# - object: The object to unregister
func unregister_object(object: ESCObject) -> void:
if not escoria.inventory_manager.inventory_has(object.global_id) \
and not object.global_id in RESERVED_OBJECTS:
objects.erase(object.global_id)

View File

@@ -1,747 +0,0 @@
extends Node
"""
This is the script that runs in background, checking for events in the events
stack and executing them.
Events are managed using 2 structures:
- event_queue: a queue for scheduled events. On each iteration, every event
in the queue is updated according time delta. If an event time occurs, it is run
(see check_event_queue()).
- levels_stack: stack of events to be run immediately (from an event in ESC file
usually)
"""
signal global_changed(global_name)
signal inventory_changed
signal open_inventory
signal saved
signal run_event(ev_name, ev_data)
signal event_done(ev_name)
signal music_volume_changed
signal action_changed
signal paused(p_paused)
onready var resource_cache = load("res://addons/escoria-core/game/core-scripts/resource_queue.gd").new()
onready var save_data = load("res://addons/escoria-core/game/core-scripts/save_data/save_data.gd").new()
# Cached scenes
var scenes_cache_list : Array = []
var scenes_cache : Dictionary = {} # this will eventually have everything in scenes_cache_list forever
# Event currently running
var running_event
# Events queue for timed events
var event_queue : Array = []
# Events stack
var levels_stack : Array = []
var reserved_globals = [
"ESC_LAST_SCENE"
]
# Dictionary of global variables
var globals : Dictionary = {}
# Dictionary of active objects
## Active == visible to the player
## Inactive == invisible to the player
var actives : Dictionary = {}
# Dictionary of all objects
## in the form : { "object global_id" : object }
var objects : Dictionary = {}
# Dictionary of all objects' states
## in the form : { "object_name" : "state_name" }
var states : Dictionary = {}
var interactives : Dictionary = {}
# Array containing the event table for each registered object
var objects_events_table : Dictionary
var continue_enabled : bool = true
var loading_game : bool = false
var game
# VERBS & TOOLS
# to be used like this:
# <current_action> <current_tool> with (target item or hotspot)
# eg: "use" "wrench" (with) (target item or hotspot)
# Current verb used
var current_action : String = "" setget set_current_action
# Current tool (ESCItem/ESCInventoryItem) used
var current_tool
## If true, we are accepting inputs
#var accept_input : bool
#enum ACCEPTABLE_INPUT {
# INPUT_NONE
#}
func _ready():
save_data.start()
get_tree().set_auto_accept_quit(ProjectSettings.get('escoria/main/force_quit'))
randomize()
save_data.check_settings()
var settings = save_data.load_settings(null)
escoria.settings = parse_json(settings)
escoria._on_settings_loaded(escoria.settings)
printt("Calling resource cache start")
resource_cache.start()
# if !ProjectSettings.get_setting("escoria/platform/skip_cache"):
# scenes_cache_list.push_back(ProjectSettings.get_setting("escoria/main/curtain"))
# scenes_cache_list.push_back(ProjectSettings.get_setting("escoria/main/hud"))
#
# printt("Cache list ", [scenes_cache_list])
# for s in scenes_cache_list:
# if s != null:
# resource_cache.queue_resource(s, false, true)
set_process(true)
func _process(delta : float):
check_event_queue(delta)
run()
check_autosave()
# Process the event queue
func check_event_queue(delta : float):
# Update every event's time in the queue
for e in event_queue:
if e.qe_time > 0:
e.qe_time -= delta
# if !can_interact() or running_event:
# return
var i = event_queue.size()
while i:
i -= 1
var queued_next = event_queue[i]
print(queued_next)
# if queued_next.qe_time <= 0:
# var obj = get_object(queued_next.qe_objname)
# run_event(obj.event_table[queued_next.qe_event])
# event_queue.remove(i)
# break
func is_debug_command(p_event):
if p_event["ev_name"] != "debug":
return false
if p_event["ev_level"].size() != 1:
return false
if !(p_event["ev_level"][0].name in escoria.esc_compiler.debug_commands):
return false
return true
# Called by run_game()
func run_event(p_event):
"""
Run an event
"""
if is_debug_command(p_event):
return run_debug_command(p_event)
else:
running_event = p_event
add_level(p_event, true)
func run_debug_command(p_event):
return callv(p_event["ev_level"][0].name, p_event["ev_level"][0].params)
func add_level(p_event, p_root : bool):
"""
Add an ESCEvent to events stack
p_event: the ESCEvent
p_root: bool
"""
levels_stack.push_back(instance_level(p_event, p_root))
return esctypes.EVENT_LEVEL_STATE.CALL
func instance_level(p_event : esctypes.ESCEvent, p_root : bool):
var new_level = {
"ip": 0, # Current instruction id
"instructions": p_event.ev_level, # List of instructions (commands)
"waiting": false, # If true, wait for current command to be finished (esc_runner_level.finished())
"break_stop": p_root,
"labels": {},
"flags": p_event.ev_flags
}
for i in range(p_event.ev_level.size()):
if p_event.ev_level[i].name == "label":
var lname = p_event.ev_level[i].params[0]
new_level.labels[lname] = i
return new_level
func run():
if levels_stack.size() == 0:
# Constantly run in _process: we may have an empty levels_stack and no event
if running_event:
emit_signal("event_done", running_event.ev_name)
running_event = null
return
while levels_stack.size() > 0:
var ret = run_top()
if ret == esctypes.EVENT_LEVEL_STATE.YIELD:
return
if ret == esctypes.EVENT_LEVEL_STATE.BREAK:
while levels_stack.size() > 0 && !(levels_stack[levels_stack.size()-1].break_stop):
levels_stack.remove(levels_stack.size()-1)
levels_stack.remove(levels_stack.size()-1)
func run_top():
var top = levels_stack[levels_stack.size()-1]
# printt("-----> TOP:", top)
var ret = $esc_level_runner.resume(top)
if ret == esctypes.EVENT_LEVEL_STATE.RETURN || ret == esctypes.EVENT_LEVEL_STATE.BREAK:
levels_stack.remove(levels_stack.size()-1)
return ret
func test(cmd):
if "if_true" in cmd.conditions.keys():
for flag in cmd.conditions.if_true:
if !get_global(flag):
return false
if "if_false" in cmd.conditions.keys():
for flag in cmd.conditions.if_false:
if get_global(flag):
return false
if "if_inv" in cmd.conditions.keys():
for flag in cmd.conditions.if_inv:
if !inventory_has(flag):
return false
if "if_not_inv" in cmd.conditions.keys():
for flag in cmd.conditions.if_not_inv:
if inventory_has(flag):
return false
if "if_active" in cmd.conditions.keys():
for flag in cmd.conditions.if_active:
if not flag in actives or not actives[flag]:
return false
if "if_not_active" in cmd.conditions.keys():
for flag in cmd.conditions.if_not_active:
if flag in actives and actives[flag]:
return false
if "if_eq" in cmd.conditions.keys():
for flag in cmd.conditions.if_eq:
if !is_global_equal_to(flag[0], flag[1]):
return false
if "if_ne" in cmd.conditions.keys():
for flag in cmd.conditions.if_ne:
if is_global_equal_to(flag[0], flag[1]):
return false
if "if_gt" in cmd.conditions.keys():
for flag in cmd.conditions.if_gt:
if !is_global_greater_than(flag[0], flag[1]):
return false
if "if_ge" in cmd.conditions.keys():
for flag in cmd.conditions.if_ge:
if is_global_less_than(flag[0], flag[1]):
return false
if "if_lt" in cmd.conditions.keys():
for flag in cmd.conditions.if_lt:
if !is_global_less_than(flag[0], flag[1]):
return false
if "if_le" in cmd.conditions.keys():
for flag in cmd.conditions.if_le:
if is_global_greater_than(flag[0], flag[1]):
return false
return true
func inventory_has(p_obj):
return get_global("i/"+p_obj)
func items_in_inventory():
var items = []
for glob in globals.keys():
if glob.begins_with("i/") and globals[glob] == "true":
items.push_back(glob.rsplit("i/", false)[0])
return items
func get_global(name):
# If no value or looks like boolean, return boolean for backwards compatibility
if not name in globals or globals[name].to_lower() == "false":
return false
if globals[name].to_lower() == "true":
return true
return globals[name]
# Set global state 'name' to 'value' (can be true or false)
# 
func set_global(name, val, force_change_reserved : bool = false):
if name in reserved_globals and !force_change_reserved:
escoria.logger.report_warnings("esc_runner.gd:set_global()",
["Global " + name + " is reserved. Value not modified."])
return
globals[name] = val
# printt("global changed at global_vm, emitting for ", name, val)
emit_signal("global_changed", name)
func set_globals(pattern : String, val):
for key in globals:
if key.match(pattern):
set_global(key, val)
# globals[key] = val
# emit_signal("global_changed", key)
func dec_global(name, diff):
var global = get_global(name)
global = int(global) if global else 0
set_global(name, str(global - diff))
func inc_global(name, diff):
var global = get_global(name)
global = int(global) if global else 0
set_global(name, str(global + diff))
func is_global_equal_to(name, val):
var global = get_global(name)
if global and val and global == val:
return true
func is_global_greater_than(name, val):
var global = get_global(name)
if global and val and int(global) > int(val):
return true
func is_global_less_than(name, val):
var global = get_global(name)
if global and val and int(global) < int(val):
return true
func jump(p_label):
while levels_stack.size() > 0:
var top = levels_stack[levels_stack.size()-1]
printt("top labels: ", top.labels, p_label)
if p_label in top.labels:
top.ip = top.labels[p_label]
return
else:
if top.break_stop || levels_stack.size() == 1:
escoria.logger.report_errors("esc_runner.gd:jump()",
["ESC: Jump to inexisting label: " + p_label])
levels_stack.remove(levels_stack.size()-1)
break
else:
levels_stack.remove(levels_stack.size()-1)
func check_autosave():
pass
func set_current_action(action : String):
if ! action is String:
escoria.logger.report_errors("esc_runner.gd",
["Trying to set_current_action: " + str(typeof(action))])
if action != current_action:
clear_current_tool()
current_action = action
emit_signal("action_changed")
func clear_current_action():
set_current_action("")
func clear_current_tool():
current_tool = null
func change_scene(params, context, run_events=true):
escoria.logger.info("Change scene to " + params[0] + " with run_events " + str(run_events))
# check_cache()
# main.clear_scene()
# camera = null
event_queue = []
escoria.main.scene_transition.fade_out()
yield(escoria.main.scene_transition, "transition_done")
# Regular events need to be reset immediately, so we don't
# accidentally `yield()` on them, for performance reasons.
# This does not affect `stack` so execution is fine anyway.
if running_event and running_event.ev_name != "load":
emit_signal("event_done", running_event.ev_name)
running_event = null
var res_room = resource_cache.get_resource(params[0])
var res_game = resource_cache.get_resource(ProjectSettings.get_setting("escoria/ui/game_scene"))
if !res_room:
escoria.logger.report_errors("esc_runner.gd:change_scene()",
["Resource not found: " + params[0]])
if !res_game:
escoria.logger.report_errors("esc_runner.gd:change_scene()",
["Resource not found: " + ProjectSettings.get_setting("escoria/ui/game_scene")])
resource_cache.clear()
# Load game scene
var game_scene = res_game.instance()
if !game_scene:
escoria.logger.report_errors("esc_runner.gd:change_scene()",
["Failed loading scene " + ProjectSettings.get_setting("escoria/ui/game_scene")])
# Load room scene
var room_scene = res_room.instance()
if room_scene:
room_scene.add_child(game_scene)
room_scene.move_child(game_scene, 0)
var events = escoria.main.set_scene(room_scene, run_events)
escoria.main.scene_transition.fade_in()
yield(escoria.main.scene_transition, "transition_done")
# If scene was never visited, add "ready" event to the events stack
if !scenes_cache.has(room_scene.global_id) \
and "ready" in events:
run_event(events["ready"])
# :setup is pretty much required in the code, but fortunately
# we can help out with cases where one isn't necessary otherwise
if not "setup" in events:
var fake_setup = escoria.esc_compiler.compile_str(":setup\n")
events["setup"] = fake_setup["setup"]
# Finally we add the setup on to of the events stack so that it is ran first
run_event(events["setup"])
escoria.inputs_manager.hotspot_focused = ""
if !scenes_cache_list.has(params[0]):
scenes_cache_list.push_back(params[0])
scenes_cache[room_scene.global_id] = params[0]
else:
escoria.logger.report_errors("esc_runner.gd:change_scene()",
["Failed loading scene " + params[0]])
if context != null:
context.waiting = false
# Re-apply actives
for active in actives:
set_active(active, actives[active])
# cam_target = null
# autosave_pending = true
func superpose_scene(params, context, run_events=true):
printt("superposing scene ", params[0], " with run_events ", run_events)
# check_cache()
# main.clear_scene()
# camera = null
# event_queue = []
# Regular events need to be reset immediately, so we don't
# accidentally `yield()` on them, for performance reasons.
# This does not affect `stack` so execution is fine anyway.
if running_event and running_event.ev_name != "load":
emit_signal("event_done", running_event.ev_name)
running_event = null
var res_room = resource_cache.get_resource(params[0])
var res_game = resource_cache.get_resource(ProjectSettings.get_setting("escoria/ui/game_scene"))
if !res_room:
escoria.logger.report_errors("esc_runner.gd:superpose_scene()",
["Resource not found: " + params[0]])
if !res_game:
escoria.logger.report_errors("esc_runner.gd:superpose_scene()",
["Resource not found: " + ProjectSettings.get_setting("escoria/ui/game_scene")])
resource_cache.clear()
# Load game scene
var game_scene = res_game.instance()
if !game_scene:
escoria.logger.report_errors("esc_runner.gd:superpose_scene()",
["Failed loading scene " + ProjectSettings.get_setting("escoria/ui/game_scene")])
# Load room scene
var room_scene = res_room.instance()
if room_scene:
get_node("/root").add_child(room_scene)
var target = context.get("set_position")
if target:
if target is Vector2:
room_scene.set_position(target)
if target is String:
var obj = get_object(target)
room_scene.set_position(obj.get_global_position())
escoria.inputs_manager.hotspot_focused = false
if !scenes_cache_list.has(params[0]):
scenes_cache_list.push_back(params[0])
if room_scene.get("global_id"):
scenes_cache[room_scene.global_id] = params[0]
else:
scenes_cache[room_scene.name] = params[0]
else:
escoria.logger.report_errors("esc_runner.gd:superpose_scene()",
["Failed loading scene " + params[0]])
if context != null:
context.waiting = false
# Re-apply actives
for active in actives:
set_active(active, actives[active])
# cam_target = null
# autosave_pending = true
func run_game(actions : Dictionary):
set_process(true)
# `load` and `ready` are exclusive because you probably don't want to
# reset the game state when a scene becomes ready, and `ready` is
# redundant when `load`ing state anyway.
# `start` is used only in your `escoria/platform/game_start_script` .esc
# file to start the game.
if "start" in actions:
clear()
run_event(actions["start"])
escoria.main_menu_instance.hide()
elif "load" in actions:
clear()
run_event(actions["load"])
elif "ready" in actions:
run_event(actions["ready"])
func clear():
get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "game_cleared")
levels_stack = []
globals = {}
objects = {}
states = {}
actives = {}
interactives = {}
event_queue = []
continue_enabled = true
loading_game = false
func register_object(name : String, val : Object, force : bool = false):
if !name:
escoria.logger.report_errors("esc_runner.gd:register_object()",
["global_id not given for " + val.get_class() + " " + val.name])
if name in objects and not force:
escoria.logger.report_errors("esc_runner.gd:register_object()",
["Trying to register already registered object " + name + ": " \
+ val.get_class() + " (" + val.name + ")"])
objects[name] = val
if not val.is_connected("tree_exited", self, "object_exit_scene"):
val.connect("tree_exited", self, "object_exit_scene", [name])
# Most objects have states/animations, but don't count on it
# if val.has_method("set_state"):
if val is ESCItem or val is ESCPlayer:
if name in states:
set_state(name, [states[name], true])
else:
set_state(name, [esctypes.OBJ_DEFAULT_STATE])
if val is ESCItem:
if val.is_interactive:
set_interactive(name, true)
if val.get("esc_script") != null and !val.get("esc_script").empty():
objects_events_table[name] = escoria.esc_compiler.load_esc_file(val.esc_script)
func get_object(name):
if !(name in objects):
return null
return objects[name]
# Activates the action for given params
# p_action String Action to execute (defined in attached ESC file and in action verbs UI)
# - eg: arrived, use, look, pickup...
# p_params Array
# - 0 Object Target object
func activate(p_action : String, p_param : Array):
escoria.logger.info("Action " + p_action + " with params ", [p_param])
# if p_param[0].global_id:
# printt("("+p_param[0].global_id+")")
var what = p_param[0]
# If we're using an action which item requires to combine
if what is ESCItem and p_action in what.combine_if_action_used_among:
# Check if object must be in inventory to be used
if what is ESCItem and what.use_from_inventory_only:
if !inventory_has(what.global_id):
# TODO Either use fallback here, or run pickup action before use
escoria.logger.report_warnings("esc_runner.gd:activate()",
["Trying to " + p_action + " on object " + what.global_id
+ " but item must be in inventory."])
return esctypes.EVENT_LEVEL_STATE.YIELD
else:
# Player has item in inventory, we check the element to use on
if p_param.size() > 1:
var combine_with = p_param[1]
var do_combine = false
if combine_with is ESCItem and combine_with.use_from_inventory_only:
if inventory_has(combine_with.global_id):
do_combine = true
else:
do_combine = true
if do_combine:
if objects_events_table[what.global_id].has(p_action + " " + combine_with.global_id):
run_event(objects_events_table[what.global_id][p_action + " " + combine_with.global_id])
return esctypes.EVENT_LEVEL_STATE.RETURN
elif objects_events_table[combine_with.global_id].has(p_action + " " + what.global_id) \
and !combine_with.combine_is_one_way:
run_event(objects_events_table[combine_with.global_id][p_action + " " + what.global_id])
return esctypes.EVENT_LEVEL_STATE.RETURN
else:
var errors = ["Attempted to execute inexisting action " + \
p_action + " between item " + combine_with.global_id + " and item " + what.global_id]
if combine_with.get("combine_is_one_way") != null \
and combine_with.combine_is_one_way:
errors.append("Reason: " + combine_with.global_id + "'s item interaction is one-way.")
escoria.logger.report_warnings("esc_runner.gd:activate()", errors)
return esctypes.EVENT_LEVEL_STATE.YIELD
else:
# TODO Use fallback here
pass
else:
# We're missing a target here.
# Tell the Label to add a conjunction and wait for another click
# to add the target to p_param. Until then, return false.
current_tool = what
return esctypes.EVENT_LEVEL_STATE.YIELD
if what.global_id in objects_events_table:
if p_action in objects_events_table[what.global_id]:
run_event(objects_events_table[what.global_id][p_action])
else:
escoria.logger.report_warnings("esc_runner.gd:activate()",
["Action '" + p_action + "' requested on object '" \
+ what.global_id + "' but action doesn't exist in attached ESC file.",
"TODO: manage fallbacks."])
return esctypes.EVENT_LEVEL_STATE.RETURN
else:
escoria.logger.report_warnings("esc_runner.gd:activate()",
["Action '" + p_action + "' requested on object '" + what.global_id \
+ "' but object does not exist in objects_events_table.", \
"Does object " + what.global_id + " have an attached ESC file?"])
return esctypes.EVENT_LEVEL_STATE.RETURN
return esctypes.EVENT_LEVEL_STATE.RETURN
func get_state(name : String):
return states[name]
func get_active(name : String) -> bool:
if actives.has(name):
return actives[name]
return false
"""
Return the interactive object given its global_id.
"""
func get_interactive(global_id : String):
if interactives.has(global_id):
return interactives[global_id]
return false
"""
Change an object state and play its animation (if it has one)
p_params[] :
- String state : the state name
- bool immediate (default=false) : if true, the animation is not played and immediately goes to the last frame
"""
func set_state(global_id : String, p_params : Array):
var obj = get_object(global_id)
states[global_id] = p_params[0]
var immediate : bool = false
if p_params.size() > 1:
immediate = p_params[1]
# A Hotspot can have a child item, if this item has an empty sprite
# (the hotspot is there to get the user input)
var animation_node
if obj is ESCItem:
#if obj.get("animation") != null:
# animation_node = obj.get("animation")
if obj.get_animation_player() != null:
animation_node = obj.get_animation_player()
if animation_node:
animation_node.stop()
var actual_animator
if animation_node is AnimationPlayer:
actual_animator = animation_node
elif animation_node is AnimatedSprite:
actual_animator = animation_node.frames
if actual_animator.has_animation(p_params[0]):
if !immediate:
animation_node.play(p_params[0])
else:
# The animation is not played, we directly set it at its last frame
if animation_node is AnimatedSprite:
animation_node.animation = p_params[0]
else:
animation_node.current_animation = p_params[0]
var animation = actual_animator.get_animation(p_params[0])
var animation_length = animation.length
animation_node.seek(animation_length)
escoria.logger.info("Item " + obj.global_id + " changed state to: " + p_params[0])
"""
When object is active, it is VISIBLE.
When object is inactive, it is HIDDEN.
"""
func set_active(name : String, active):
actives[name] = active
if objects.has(name) and is_instance_valid(objects[name]):
if objects[name] is ESCInventoryItem:
return
if active:
objects[name].show()
else:
objects[name].hide()
"""
When object is interactive, it can be focused
When object is not interactive, it cannot be focused and used
"""
func set_interactive(name : String, active):
interactives[name] = active
"""
Callback called by ESCItems when it emits "tree_exit", ie. removed from scene.
Item is kept in objects[] array if it is in inventory.
"""
func object_exit_scene(name : String):
# If object is in inventory, save it before it's destroyed so we still have
# its data in objects[]
if inventory_has(name):
objects[name] = objects[name].duplicate()
else:
if name != "bg_music":
escoria.logger.info("Object " + name + " removed from scene.")
objects.erase(name)
func check_obj(name, cmd):
var obj = escoria.esc_runner.get_object(name)
if obj == null:
escoria.logger.report_errors("esc_runner.gd:check_obj()",
["Global id "+name+" not found for " + cmd])
return false
return true

View File

@@ -1,773 +0,0 @@
extends Node
# This script implements the ESCCommands contained in the ESCEvent.
# TODO : this script should use "Command" pattern.
var current_context
func finished(context = null):
if context != null:
context.waiting = false
else:
current_context.waiting = false
if escoria.current_state == escoria.GAME_STATE.WAIT:
escoria.current_state = escoria.GAME_STATE.DEFAULT
func resume(context):
current_context = context
if context.waiting:
return esctypes.EVENT_LEVEL_STATE.YIELD
var count = context.instructions.size()
while context.ip < count:
var top = escoria.esc_runner.levels_stack.size()
var ret = run(context)
context.ip += 1
if top < escoria.esc_runner.levels_stack.size():
return esctypes.EVENT_LEVEL_STATE.CALL
if ret == esctypes.EVENT_LEVEL_STATE.YIELD:
return esctypes.EVENT_LEVEL_STATE.YIELD
if ret == esctypes.EVENT_LEVEL_STATE.CALL:
return esctypes.EVENT_LEVEL_STATE.CALL
if ret == esctypes.EVENT_LEVEL_STATE.BREAK:
if context.break_stop:
break
else:
return esctypes.EVENT_LEVEL_STATE.BREAK
if ret == esctypes.EVENT_LEVEL_STATE.REPEAT:
context.ip = 0
if ret == esctypes.EVENT_LEVEL_STATE.JUMP:
return esctypes.EVENT_LEVEL_STATE.JUMP
context.ip = 0
return esctypes.EVENT_LEVEL_STATE.RETURN
func run(context):
var cmd = context.instructions[context.ip]
if cmd.name == "label":
return esctypes.EVENT_LEVEL_STATE.RETURN
if !escoria.esc_runner.test(cmd):
return esctypes.EVENT_LEVEL_STATE.RETURN
#print("name is ", cmd.name)
#if !(cmd.name in self):
# escoria.logger.report_errors("", ["Unexisting command "+cmd.name])
return call(cmd.name, cmd.params)
"""
Automatically called when a dialog line is said.
"""
func dialog_line_finished() -> void:
# escoria.esc_runner.get_node("esc_level_runner").finished()
finished()
escoria.dialog_player.is_speaking = false
escoria.current_state = escoria.GAME_STATE.DEFAULT
"""
accept_input [ALL|NONE|SKIP]
What type of input does the game accept. ALL is the default, SKIP allows skipping
of dialog but nothing else, NONE denies all input. Including opening the menu etc.
SKIP and NONE also disable autosaves. Note that SKIP gets reset to ALL when the
event is done, but NONE persists. This allows you to create cut scenes with SKIP
where the dialog can be skipped, but also initiate locked-down cutscenes with
accept_input NONE in :setup and accept_input ALL later in :ready.
"""
func accept_input(command_params : Array):
# var p_input = command_params[0]
# var input = escoria.esc_runner.acceptable_inputs["INPUT_" + p_input]
# escoria.esc_runner.set_accept_input(input)
pass
"""
"""
func autosave():
# escoria.request_autosave()
pass
"""
anim object name [reverse] [flip_x] [flip_y]
Executes the animation specificed with the "name" parameter on the object,
without blocking. The next command in the event will be executed immediately after.
Optional parameters:
reverse plays the animation in reverse when true
flip_x flips the x axis of the object's sprites when true (object's root node needs to be Node2D)
flip_y flips the y axis of the object's sprites when true (object's root node needs to be Node2D)
"""
func anim(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "anim"):
return esctypes.EVENT_LEVEL_STATE.RETURN
private_play_animation(command_params)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
Groups Commands can be grouped using the character ">" to start a group, and
incrementing the indentation of the commands that belong to the group. Example:
>
set_global door_open true
animation player pick_up
# end of group
"""
func branch(command_params : Array):
var branch_ev = esctypes.ESCEvent.new("branch", command_params, [])
return escoria.esc_runner.add_level(branch_ev, false)
"""
camera_push target [time] [type]
Push camera to target. Target must have camera_pos set.
If it's of type Camera2D, its zoom will be used as well as position.
- A time value of 0 will set the camera immediately.
- type is any of the Tween.TransitionType values without the prefix, eg. LINEAR,
QUART or CIRC; defaults to QUART.
"""
func camera_push(command_params : Array):
var target = escoria.esc_runner.get_object(command_params[0])
var time = command_params[1] if command_params.size() > 1 else 1
var type = command_params[2] if command_params.size() > 2 else "QUAD"
escoria.esc_runner.get_object("camera").push(target, time, type)
"""
camera_set_limits camlimits_id
Sets the camera limits to the one defined under "camlimits_id" in ESCRoom's
camera_limits array.
- camlimits_id : int : id of the camera limits to apply (defined in ESCRoom's
camera_limits array)
"""
func camera_set_limits(command_params : Array):
escoria.main.set_camera_limits(command_params[0])
"""
camera_set_drag_margin_enabled horizontal_enabled vertical_enabled
- horizontal_enabled : bool
- vertical_enabled : bool are booleans for whether or not horizontal and vertical drag
margins are enabled. You will likely want to set them false for advanced camera
motions and true for regular gameplay and/or tracking NPCs.
"""
func camera_set_drag_margin_enabled(command_params : Array):
var horizontal_enabled = command_params[0]
var vertical_enabled = command_params[1]
escoria.esc_runner.get_object("camera").set_drag_margin_enabled(horizontal_enabled, vertical_enabled)
"""
camera_set_pos speed x y
Moves the camera to a position defined by "x" and "y", at the speed defined by
"speed" in pixels per second. If speed is 0, camera is teleported to the position.
"""
func camera_set_pos(command_params : Array):
var speed = command_params[0]
var pos = Vector2(command_params[1], command_params[2])
escoria.esc_runner.get_object("camera").set_target(pos, speed)
"""
camera_set_target speed object [object2 object3 ...]
Configures the camera to follow 1 or more objects, using "speed" as speed limit.
This is the default behavior (default follow object is "player").
If there's more than 1 object, the camera follows the average position of all
the objects specified.
"""
func camera_set_target(command_params : Array):
var speed = command_params[0]
var target = escoria.esc_runner.get_object(command_params[1])
escoria.esc_runner.get_object("camera").set_target(target, speed)
"""
camera_set_zoom magnitude [time]
Zooms the camera in/out to the desired magnitude. Values larger than 1 zooms
the camera out, and smaller values zooms in, relative to the default value of 1.
An optional time in seconds controls how long it takes for the camera to zoom
into position.
"""
func camera_set_zoom(command_params : Array):
var zoom_level = command_params[0]
var speed = command_params[0] if command_params.size() > 1 else 0
escoria.esc_runner.get_object("camera").set_camera_zoom(zoom_level, speed)
"""
camera_set_zoom_height pixels [time]
Similar to the command above, but uses pixel height instead of magnitude to zoom.
"""
func camera_set_zoom_height(command_params : Array):
var magnitude = command_params[0] / escoria.game_size.y
var time = command_params[1] if command_params.size() > 1 else 0
escoria.esc_runner.get_object("camera").set_camera_zoom(magnitude, float(time))
"""
camera_shift x y [time] [type]
Shift camera by x and y pixels over time seconds.
- type is any of the Tween.TransitionType values without the prefix, eg. LINEAR,
QUART or CIRC; defaults to QUART.
"""
func camera_shift(command_params : Array):
var x = command_params[0]
var y = command_params[1]
var time = command_params[2]
var type = command_params[3] if command_params.size() > 3 else "QUAD"
escoria.esc_runner.get_object("camera").shift(x, y, time, type)
"""
change_scene path [run_events]
Loads a new scene, specified by "path".
The run_events variable is a boolean (default true) which you never want to set
manually! It's there only to benefit save games, so they don't conflict with the
scene's events.
"""
func change_scene(command_params : Array):
# Savegames must have events disabled, so saving the game adds a false to params
var run_events = true
if command_params.size() == 2:
run_events = bool(command_params[1])
# looking for localized string format in scene. this should be somewhere else
var sep = command_params[0].find(":\"")
if sep >= 0:
var path = command_params[0].substr(sep + 2, command_params[0].length() - (sep + 2))
escoria.esc_runner.call_deferred("change_scene", [path], current_context, run_events)
else:
escoria.esc_runner.call_deferred("change_scene", command_params, current_context, run_events)
current_context.waiting = true
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
"""
func custom():
pass
"""
cut_scene object name [reverse] [flip_x] [flip_y]
Executes the animation specificed with the "name" parameter on the object, BLOCKING.
The next command in the event will be executed when the animation is finished
playing.
Optional parameters:
- reverse plays the animation in reverse when true
- flip_x flips the x axis of the object's sprites when true
(object's root node needs to be Node2D)
- flip_y flips the y axis of the object's sprites when true
(object's root node needs to be Node2D)
"""
func cut_scene(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "cut_scene"):
return esctypes.EVENT_LEVEL_STATE.RETURN
private_play_animation(command_params)
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
PRIVATE
Play animation using parameters.
Used by commands anim() and cut_scene()
"""
func private_play_animation(command_params : Array):
var obj = escoria.esc_runner.get_object(command_params[0])
var anim_id = command_params[1]
var reverse = false
if command_params.size() > 2:
reverse = command_params[2]
var flip = Vector2(1, 1)
if command_params.size() > 3 && command_params[3]:
flip.x = -1
if command_params.size() > 4 && command_params[4]:
flip.y = -1
current_context.waiting = true
obj.play_anim(anim_id, current_context, reverse, flip)
"""
debug string [string2 ...]
Takes 1 or more strings, prints them to the console.
"""
func debug(command_params : Array):
for p in command_params:
printt(p)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
dec_global name value
Subtracts the value from global with given "name".
Value and global must both be integers.
"""
func dec_global(command_params : Array):
escoria.esc_runner.dec_global(command_params[0], command_params[1])
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
inc_global name value
Adds the value to global with given "name".
Value and global must both be integers.
"""
func inc_global(command_params : Array):
escoria.esc_runner.inc_global(command_params[0], command_params[1])
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
Start a dialog choice.
"""
func dialog(command_params : Array):
current_context.waiting = true
current_context.in_dialog = true
escoria.current_state = escoria.GAME_STATE.DIALOG
if !escoria.dialog_player:
escoria.dialog_player = escoria.main.current_scene.get_node("game/ui/dialog_layer/dialog_player")
var options = command_params.slice(1, command_params.size())
escoria.dialog_player.start_dialog_choices(command_params[0], options)
return esctypes.EVENT_LEVEL_STATE.YIELD
#func dialog_config():
## escoria.esc_runner.dialog_config(params)
## return esctypes.EVENT_LEVEL_STATE.RETURN
# pass
"""
enable_terrain node_name
Enable the ESCTerrain's NavigationPolygonInstance defined by given node name.
Disables previously activated NavigationPolygonInstance.
"""
func enable_terrain(command_params : Array):
var name : String = command_params[0]
if escoria.room_terrain.has_node(name):
var new_active_navigation_instance = escoria.room_terrain.get_node(name)
escoria.room_terrain.current_active_navigation_instance.enabled = false
escoria.room_terrain.current_active_navigation_instance = new_active_navigation_instance
escoria.room_terrain.current_active_navigation_instance.enabled = true
"""
"""
func game_over(command_params : Array):
pass
"""
Adds element in inventory.
Usage: inventory_add my_item
equivalent to: set_global i/my_item true
"""
func inventory_add(command_params : Array):
set_global(["i/"+command_params[0], "true"])
"""
Removes element from inventory.
Usage: inventory_remove my_item
equivalent to: set_global i/my_item false
"""
func inventory_remove(command_params : Array):
set_global(["i/"+command_params[0], "false"])
"""
inventory_open true/false
Shows or hides inventory. Uses code in game.tscn scene, thus developed outside of Escoria.
"""
func inventory_display(command_params : Array):
var display : bool = bool(command_params[0])
if display:
escoria.main.current_scene.game.open_inventory()
else:
escoria.main.current_scene.game.close_inventory()
"""
jump label_name
Jump to label_name block. This is used to build more complex dialog trees.
Labels must be defined at the same level as the 'jump' command, using either
'label label_name' command or '% label_name'.
"""
func jump(command_params : Array):
escoria.esc_runner.jump(command_params[0])
return esctypes.EVENT_LEVEL_STATE.JUMP
"""
set_sound_state bg_music|bg_sound off|default|<path/to/music.file> true|false
"""
func set_sound_state(command_params : Array):
var snd_player = command_params[0]
var snd_id = command_params[1]
var loop = false
if command_params.size() == 3 and command_params[2]:
loop = true
escoria.main.get_node(snd_player).set_state(snd_id, loop)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
queue_animation object animation
"""
func queue_animation(command_params : Array):
pass
"""
queue_resource path [front_of_queue]
Queues the load of a resource in a background thread. The path must be a full
path inside your game, for example "res://scenes/next_scene.tscn".
The "front_of_queue" parameter is optional (default value false), to put the
resource in the front of the queue. Queued resources are cleared when a change
scene happens (but after the scene is loaded, meaning you can queue resources
that belong to the next scene).
"""
func queue_resource(command_params : Array):
var path : String = command_params[0]
var in_front : bool = command_params[1] if command_params.size() > 1 else false
escoria.esc_runner.resource_cache.queue_resource(path, in_front)
"""
repeat
Restarts the execution of the current scope at the start. A scope can be a group
or an event.
"""
func repeat(command_params : Array):
return esctypes.EVENT_LEVEL_STATE.REPEAT
"""
say speaker_id "text_to_say" [dialog_ui_name]
Make a character say one line.
- dialog_ui_name String if set, uses the dialog UI by its name as defined
in game.tscn/dialog_layer/dialog_player
"""
func say(command_params : Array) -> esctypes:
current_context.waiting = true
var dict : Dictionary
var dialog_scene_name = ProjectSettings.get_setting("escoria/ui/default_dialog_scene")
if dialog_scene_name.empty():
escoria.logger.report_errors("level_esc_runners.gd:say()",
["Project setting 'escoria/ui/default_dialog_scene' is not set.",
"Please set a default dialog scene."])
var file = dialog_scene_name.get_file()
var extension = dialog_scene_name.get_extension()
dialog_scene_name = file.rstrip("." + extension)
# Manage specific dialog scene
if command_params.size() > 2:
dialog_scene_name = command_params[2]
# Manage translation/voice lines keys in the form of :
# line_key:"Default line text"
# If a line_key exists, we'll set it a label as it will automatically be
# translated
var dialog_key_line = command_params[1].split(":", true, 1)
if dialog_key_line.size() > 1:
dialog_key_line[1] = dialog_key_line[1].trim_prefix("\"")
dict = {
"key": dialog_key_line[0],
"line": dialog_key_line[1] if dialog_key_line.size() > 1 else dialog_key_line[0],
"ui": dialog_scene_name
#"ui": "dialog_label"
#"ui": "dialog_box_inset"
}
escoria.current_state = escoria.GAME_STATE.DIALOG
if !escoria.dialog_player:
escoria.dialog_player = escoria.main.current_scene.get_node("game/ui/dialog_layer/dialog_player")
escoria.dialog_player.say(command_params[0], dict)
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
set_active global_id true/false
Sets object as active or inactive. Active objects are displayed in scene and
respond to inputs. Inactives are hidden.
"""
func set_active(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "set_active"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var name : String = command_params[0]
var value = command_params[1]
escoria.esc_runner.set_active(name, value)
"""
set_angle object_id angle_degrees
Set the angle of an object.
"""
func set_angle(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "set_angle"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var obj = escoria.esc_runner.get_object(command_params[0])
# HACK Countering the fact that angle_to_point() function gives
# angle against X axis not Y, we need to check direction using (angle-90°).
# Since the ESC command already gives the right angle, we add 90.
obj.set_angle(int(command_params[1] + 90))
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
set_state object_id state_name
Set object state to state_name. States are recorded in escoria.states[].
If the object has an animation named 'state_name', Escoria plays it.
"""
func set_state(command_params : Array):
var global_id : String = command_params[0]
var p_params : Array = command_params.slice(1, command_params.size())
escoria.esc_runner.set_state(global_id, p_params)
"""
set_hud_visible true/false
Hides the UI. Uses code in game.tscn scene, thus developed outside of Escoria.
"""
func set_hud_visible(command_params : Array):
if command_params[0]:
escoria.main.current_scene.game.show_ui()
else:
escoria.main.current_scene.game.hide_ui()
"""
"""
func sched_event(command_params : Array):
pass
"""
set_global global_variable value
Sets a global variable value. Value can be string, integer or boolean.
"""
func set_global(command_params : Array):
var name : String = command_params[0]
var value = command_params[1]
escoria.esc_runner.set_global(name, value)
"""
set_globals pattern value
Changes the value of multiple globals using a wildcard pattern.
Example:
# clears the inventory
set_globals i/* false
"""
func set_globals(command_params : Array):
var pattern : String = command_params[0]
var val = command_params[1]
escoria.esc_runner.set_globals(pattern, val)
"""
set_interactive global_id true/false
Sets object as interactive or not. Interactive objects can be pointed and
interacted with.
"""
func set_interactive(command_params : Array):
var global_id : String = command_params[0]
var value = command_params[1]
escoria.esc_runner.set_interactive(global_id, value)
"""
set_speed global_id speed_value
Sets the speed of object defined by 'global_id' to 'speed_value'
"""
func set_speed(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "set_speed"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var global_id : String = command_params[0]
var speed_value = command_params[1]
escoria.get_object(global_id).set_speed(speed_value)
"""
"""
func slide(command_params : Array):
pass
"""
"""
func slide_to_pos(command_params : Array):
pass
"""
"""
func slide_block(command_params : Array):
pass
"""
"""
func slide_to_pos_block(command_params : Array):
pass
"""
spawn path [object2]
Instances a scene determined by "path", and places in the position of object2
(object2 is optional)
"""
func spawn(command_params : Array):
superpose_scene(command_params)
"""
stop
Stops the execution of the current script when it reaches the 'stop' instruction.
Usually used in the end of commands blocks.
"""
func stop(command_params : Array):
return esctypes.EVENT_LEVEL_STATE.BREAK
"""
superpose_scene path [run_events]
Loads a new scene, specified by "path" and displays it OVER the current one.
This is useful to display puzzle scenes over the current room, so that you don't
loose any progression and continuity.
- path String Path to the scene to superpose.
- run_events Boolean (default true) which you never want to set
manually! It's there only to benefit save games, so they don't conflict with the
scene's events.
"""
func superpose_scene(command_params : Array):
# Savegames must have events disabled, so saving the game adds a false to params
var run_events = true
if command_params.size() == 2:
if command_params[1] in ["true", "false"]:
run_events = true if command_params[1] == "true" else false
else:
current_context["set_position"] = command_params[1]
# looking for localized string format in scene. this should be somewhere else
var sep = command_params[0].find(":\"")
if sep >= 0:
var path = command_params[0].substr(sep + 2, command_params[0].length() - (sep + 2))
escoria.esc_runner.call_deferred("superpose_scene", [path], current_context, run_events)
else:
escoria.esc_runner.call_deferred("superpose_scene", command_params, current_context, run_events)
current_context.waiting = true
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
teleport obj1 obj2 [angle_degrees]
Teleports obj1 at obj2's position. If angle_degrees is set (int), sets obj1's
angle to angle_degrees.
"""
func teleport(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "teleport"):
return esctypes.EVENT_LEVEL_STATE.RETURN
if !escoria.esc_runner.check_obj(command_params[1], "teleport"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var angle
if command_params.size() > 2:
angle = int(command_params[2])
escoria.esc_runner.get_object(command_params[0]) \
.teleport(escoria.esc_runner.get_object(command_params[1]), angle)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
teleport obj1 x y [angle_degrees]
Teleports obj1 at (x,y). If angle_degrees is set (int), sets obj1's
angle to angle_degrees.
"""
func teleport_pos(command_params : Array):
if !escoria.esc_runner.check_obj(command_params[0], "teleport"):
return esctypes.EVENT_LEVEL_STATE.RETURN
var x = command_params[1]
var y = command_params[2]
var angle
if command_params.size() > 2:
angle = int(command_params[3])
escoria.esc_runner.get_object(command_params[0]).teleport(Vector2(x,y), angle)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
"""
func turn_to(command_params : Array):
pass
"""
Wait for given time in seconds.
Usage: wait time_in_seconds
"""
func wait(command_params : Array):
escoria.current_state = escoria.GAME_STATE.WAIT
var time = float(command_params[0])
if time <= 0:
return esctypes.EVENT_LEVEL_STATE.RETURN
# get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "wait", time, p_level)
escoria.main.wait(command_params, current_context)
current_context.waiting = true
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
walk object_id1 object_id2
Make object1 walk towards object2. This command is not blocking (user input not disabled)
"""
func walk(command_params : Array):
current_context.waiting = false
escoria.do("walk", command_params)
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
walk_block object_id1 object_id2
Make object1 walk towards object2. This command is blocking (user input disabled)
"""
func walk_block(command_params : Array):
current_context.waiting = true
escoria.do("walk", command_params)
return esctypes.EVENT_LEVEL_STATE.YIELD
"""
walk_to_pos object_id1 pos_x pos_y
Make object1 walk towards object2. This command is not blocking (user input not disabled)
"""
func walk_to_pos(command_params : Array):
current_context.waiting = false
var destination_pos = Vector2(command_params[1], command_params[2])
escoria.do("walk", [command_params[0], destination_pos])
return esctypes.EVENT_LEVEL_STATE.RETURN
"""
walk_to_pos_block object_id1 pos_x pos_y
Make object1 walk towards object2. This command is blocking (user input disabled)
"""
func walk_to_pos_block(command_params : Array):
current_context.waiting = true
var destination_pos = Vector2(command_params[1], command_params[2])
escoria.do("walk", [command_params[0], destination_pos])
return esctypes.EVENT_LEVEL_STATE.YIELD

View File

@@ -0,0 +1,21 @@
# A base class for every ESC command.
# Extending classes have to override the configure and run function
extends Node
class_name ESCBaseCommand
# Return the descriptor of the arguments of this command
func configure() -> ESCCommandArgumentDescriptor:
escoria.logger.error("Command %s did not override configure." % get_class())
return ESCCommandArgumentDescriptor.new()
# Validate wether the given arguments match the command descriptor
func validate(arguments: Array) -> bool:
return self.configure().validate(get_class(), arguments)
# Run the command
func run(command_params: Array) -> int:
escoria.logger.error("Command %s did not override run." % get_class())
return 0

View File

@@ -0,0 +1,124 @@
# An ESC command
extends ESCStatement
class_name ESCCommand
# Regex matching command lines
const REGEX = \
'^(\\s*)(?<name>[^\\s]+)(\\s(?<parameters>([^\\[]|$)+))?' +\
'(\\[(?<conditions>[^\\]]+)\\])?'
# The name of this command
var name: String
# Parameters of this command
var parameters: Array = []
# A list of ESCConditions to run this command.
# Conditions are combined using logical AND
var conditions: Array = []
# Create a command from a command string
func _init(command_string):
var command_regex = RegEx.new()
command_regex.compile(REGEX)
if command_regex.search(command_string):
for result in command_regex.search_all(command_string):
if "name" in result.names:
self.name = escoria.utils.get_re_group(result, "name")
if "parameters" in result.names:
# Split parameters by whitespace but allow quoted
# parameters
var quote_open = false
var parameter_values = PoolStringArray([])
var parsed_parameters = \
escoria.utils.sanitize_whitespace(
escoria.utils.get_re_group(
result,
"parameters"
).strip_edges()
)
for parameter in parsed_parameters.split(" "):
if parameter.begins_with('"') and parameter.ends_with('"'):
parameters.append(
parameter.substr(1, parameter.length() - 2)
)
elif parameter.begins_with('"'):
quote_open = true
parameter_values.append(parameter.substr(1))
elif parameter.ends_with('"'):
quote_open = false
parameter_values.append(
parameter.substr(0, len(parameter) - 1)
)
parameters.append(parameter_values.join(" "))
parameter_values.resize(0)
elif quote_open:
parameter_values.append(parameter)
else:
parameters.append(parameter)
if "conditions" in result.names:
for condition in escoria.utils.get_re_group(
result,
"conditions"
).split(","):
self.conditions.append(
ESCCondition.new(condition.strip_edges())
)
else:
escoria.logger.report_errors(
"Invalid command detected: %s" % command_string,
[
"Command regexp didn't match"
]
)
# Check, if conditions match
func is_valid() -> bool:
var command_found = false
for base_path in ProjectSettings.get("escoria/esc/command_directories"):
var command_path = "%s/%s.gd" % [
base_path,
self.name
]
if ResourceLoader.exists(command_path):
command_found = true
if not command_found:
escoria.logger.report_errors(
"Invalid command detected: %s" % self.name,
[
"Command implementation not found in any command directory"
]
)
return false
return .is_valid()
# Run this command
func run() -> int:
var command_object = escoria.command_registry.get_command(self.name)
if command_object == null:
return ESCExecution.RC_ERROR
else:
var argument_descriptor = command_object.configure()
var prepared_arguments = argument_descriptor.prepare_arguments(
self.parameters
)
if argument_descriptor.validate(self.name, prepared_arguments):
escoria.logger.debug("Running command %s with parameters %s" % [
self.name,
prepared_arguments
])
var rc = command_object.run(prepared_arguments)
if rc is GDScriptFunctionState:
rc = yield(rc, "completed")
escoria.logger.debug("[%s] Return code: %d" % [self.name, rc])
return rc
else:
return ESCExecution.RC_ERROR

View File

@@ -0,0 +1,90 @@
# The descriptor of the arguments of an ESC command
extends Object
class_name ESCCommandArgumentDescriptor
# Number of arguments the command expects
var min_args: int = 0
# The types the arguments as TYPE_ constants. If the command is called with
# more arguments than there are entries in the types array, the additional
# arguments will be checked against the last entry of the types array.
var types: Array = []
# The default values for the arguments
var defaults: Array = []
# Initialize the descriptor
func _init(p_min_args: int = 0, p_types: Array = [], p_defaults: Array = []):
min_args = p_min_args
types = p_types
defaults = p_defaults
# Combine the default argument values with the given arguments
func prepare_arguments(arguments: Array) -> Array:
var complete_arguments = defaults
for index in range(arguments.size()):
complete_arguments[index] = escoria.utils.get_typed_value(
arguments[index]
)
return complete_arguments
# Validate wether the given arguments match the command descriptor
func validate(command: String, arguments: Array) -> bool:
if arguments.size() < self.min_args:
escoria.logger.report_errors(
"Invalid command arguments for command %s" % command,
[
"Arguments didn't match minimum size %d: %s" %
self.min_args,
arguments
]
)
for index in range(arguments.size()):
if arguments[index] == null:
# No type checking for null values
continue
var correct = false
var types_index = index
if types_index > types.size():
types_index = types.size() - 1
if not self.types[types_index] is Array:
self.types[types_index] = [self.types[index]]
for type in self.types[types_index]:
if not correct:
correct = self._is_type(arguments[index], type)
if not correct:
escoria.logger.report_errors(
"Argument type did not match descriptor for command %s" %
command,
[
"Argument %d is of type %d. Expected %s" % [
index,
typeof(arguments[index]),
PoolStringArray(
self.types[types_index]
).join(",")
]
]
)
return true
# Check wether the given argument is of the given type
#
# #### Parameters
#
# - argument: Argument to test
# - type: Type to check
# *Returns* Wether the argument is of the given type
func _is_type(argument, type: int) -> bool:
return typeof(argument) == type

View File

@@ -0,0 +1,130 @@
# A condition to run a command
extends Object
class_name ESCCondition
# Valid comparison types
enum {COMPARISON_NONE, COMPARISON_EQ, COMPARISON_GT, COMPARISON_LT}
# Regex that matches condition lines
const REGEX = \
'^(?<is_negated>!)?(?<comparison>eq|gt|lt)? ?' +\
'(?<is_inventory>i\/)?(?<flag>[^ ]+)( (?<comparison_value>.+))?$'
const COMPARISON_DESCRIPTION = [
"Checking if %s %s %s true%s",
"Checking if %s %s %s equals %s",
"Checking if %s %s %s greater than %s",
"Checking if %s %s %s less than %s"
]
# Name of the flag compared
var flag: String
# Wether this condition is negated
var negated: bool = false
# Wether this condition is regarding an inventory item ("i/...")
var inventory: bool = false
# An optional comparison type. Use the COMPARISON-Enum
var comparison: int = COMPARISON_NONE
# The value used together with the comparison type
var comparison_value
# Create a new condition from an ESC condition string
func _init(comparison_string: String):
var comparison_regex = RegEx.new()
comparison_regex.compile(
REGEX
)
if comparison_regex.search(comparison_string):
for result in comparison_regex.search_all(comparison_string):
if "is_negated" in result.names:
self.negated = true
if "comparison" in result.names:
match escoria.utils.get_re_group(result, "comparison"):
"eq": self.comparison = COMPARISON_EQ
"gt": self.comparison = COMPARISON_GT
"lt": self.comparison = COMPARISON_LT
_: escoria.logger.report_errors(
"Invalid comparison type detected: %s" %
comparison_string,
[
"Comparison type %s unknown" %
escoria.utils.get_re_group(
result,
"comparison"
)
]
)
if "comparison_value" in result.names:
self.comparison_value = escoria.utils.get_typed_value(
escoria.utils.get_re_group(
result,
"comparison_value"
)
)
if "is_inventory" in result.names:
self.inventory = true
if "flag" in result.names:
self.flag = escoria.utils.get_re_group(result, "flag")
else:
escoria.logger.report_errors(
"Invalid comparison detected: %s" % comparison_string,
[
"Comparison regexp didn't match"
]
)
# Run this comparison against the globals
func run() -> bool:
var global_name = self.flag
escoria.logger.debug(
COMPARISON_DESCRIPTION[self.comparison] % [
"inventory item" if self.inventory else "global value",
self.flag,
"is not" if self.negated else "is",
"" if self.comparison == COMPARISON_NONE else self.comparison_value
]
)
if self.inventory:
global_name = "i/%s" % flag
var return_value = false
if self.comparison == COMPARISON_NONE and \
escoria.globals_manager.has(global_name) and \
escoria.globals_manager.get_global(global_name) is bool and \
escoria.globals_manager.get_global(global_name):
return_value = true
elif self.comparison == COMPARISON_EQ and \
escoria.globals_manager.get_global(global_name) == \
self.comparison_value:
return_value = true
elif self.comparison == COMPARISON_GT and \
escoria.globals_manager.get_global(global_name) > \
self.comparison_value:
return_value = true
elif self.comparison == COMPARISON_LT and \
escoria.globals_manager.get_global(global_name) < \
self.comparison_value:
return_value = true
if self.negated:
return_value = not return_value
escoria.logger.debug(
"It is" if return_value else "It isn't"
)
return return_value

View File

@@ -0,0 +1,84 @@
# An ESC dialog
extends ESCStatement
class_name ESCDialog
# Regex that matches dialog lines
const REGEX = \
'^(\\s*)\\?( (?<type>[^ ]+))?( (?<avatar>[^ ]+))?' +\
'( (?<timeout>[^ ]+))?( (?<timeout_option>.+))?$'
# A Regex that matches the end of a dialog
const END_REGEX = \
'^(?<indent>\\s*)!.*$'
# Dialog type
var type: String = ""
# Avatar used in the dialog
var avatar: String = ""
# Timeout until the timeout_option option is selected. Use 0 for no timeout
var timeout: int = 0
# The dialog option to select when timeout is reached
var timeout_option: int = 0
# A list of ESCDialogOptions
var options: Array
# Construct a dialog from a dialog string
func _init(dialog_string: String):
var dialog_regex = RegEx.new()
dialog_regex.compile(REGEX)
if dialog_regex.search(dialog_string):
for result in dialog_regex.search_all(dialog_string):
if "type" in result.names:
self.type = escoria.utils.get_re_group(result, "type")
if "avatar" in result.names:
self.avatar = escoria.utils.get_re_group(result, "avatar")
if "timeout" in result.names:
self.timeout = int(
escoria.utils.get_re_group(result, "timeout")
)
if "timeout_option" in result.names:
self.timeout_option = int(
escoria.utils.get_re_group(result, "timeout_option")
)
else:
escoria.logger.report_errors(
"Invalid dialog detected: %s" % dialog_string,
[
"Dialog regexp didn't match"
]
)
# Dialogs have no conditions, just return true
func is_valid() -> bool:
return true
# Run this dialog
func run():
escoria.logger.debug("Starting dialog")
escoria.current_state = escoria.GAME_STATE.DIALOG
if !escoria.dialog_player:
escoria.dialog_player = escoria.main.current_scene.get_node(
"game/ui/dialog_layer/dialog_player"
)
escoria.dialog_player.start_dialog_choices(self)
var option = yield(
escoria.dialog_player,
"option_chosen"
) as ESCDialogOption
var rc = option.run()
if rc is GDScriptFunctionState:
rc = yield(rc, "completed")
if rc != ESCExecution.RC_CANCEL:
return self.run()
return rc

View File

@@ -0,0 +1,49 @@
# An option of an ESC dialog
extends ESCStatement
class_name ESCDialogOption
# Regex that matches dialog option lines
const REGEX = \
'^[^-]*- "(?<option>[^"]+)"( \\[(?<conditions>[^\\]]+)\\])?$'
# Option displayed in the HUD
var option: String
# Conditions to show this dialog
var conditions: Array = []
# Create a dialog option from a string
func _init(option_string: String):
var option_regex = RegEx.new()
option_regex.compile(REGEX)
if option_regex.search(option_string):
for result in option_regex.search_all(option_string):
if "option" in result.names:
self.option = escoria.utils.get_re_group(result, "option")
if "conditions" in result.names:
for condition_text in escoria.utils.get_re_group(
result,
"conditions"
).split(","):
self.conditions.append(
ESCCondition.new(condition_text.strip_edges())
)
else:
escoria.logger.report_errors(
"Invalid dialog option detected: %s" % option_string,
[
"Dialog option regexp didn't match"
]
)
# Check, if conditions match
func is_valid() -> bool:
for condition in self.conditions:
if not (condition as ESCCondition).run():
return false
return true

View File

@@ -0,0 +1,93 @@
# An event in the ESC language
#
# Events are triggered from various sources. Common events include
#
# * :setup Called every time when visiting a scene
# * :ready Called the first time a scene is visited
# * :use <global id> Called from the current item when it is used with the item
# with the global id <global id>
extends ESCStatement
class_name ESCEvent
# Regex identifying an ESC event
const REGEX = \
'^:(?<name>[^|]+)( \\|(?<flags>( ' + \
'(TK|NO_TT|NO_HUD|NO_SAVE|CUT_BLACK|LEAVE_BLACK)' + \
')+))?$'
# Valid event flags
# * TK: stands for "telekinetic". It means the player won't walk over to
# the item to say the line.
# * NO_TT: stands for "No tooltip". It hides the tooltip for the duration of
# the event. Probably not very useful, because events having multiple
# say commands in them are automatically hidden.
# * NO_HUD: stands for "No HUD". It hides the HUD for the duration of the
# event. Useful when you want something to look like a cut scene but not
# disable input for skipping dialog.
# * NO_SAVE: disables saving. Use this in cut scenes and anywhere a
# badly-timed autosave would leave your game in a messed-up state.
# * CUT_BLACK: applies only to `:setup`. It makes the screen go black
# during the setup phase. You will probably see a quick black flash, so use
# it only if you prefer it over the standard cut.
# * LEAVE_BLACK: applies only to `:setup`. In case your `:ready` starts with
# `cut_scene telon fade_in`, you must apply this flag or you will see a
# flash of your new scene before going black again for the fade_in.
enum {
FLAG_TK = 1,
FLAG_NO_TT = 2,
FLAG_NO_HUD = 4,
FLAG_NO_SAVE = 8,
FLAG_CUT_BLACK = 16,
FLAG_LEAVE_BLACK = 32
}
# Name of event
var name: String
# Flags set to this event
var flags: int = 0
# Create a new event from an event line
func _init(event_string: String):
var event_regex = RegEx.new()
event_regex.compile(REGEX)
if event_regex.search(event_string):
for result in event_regex.search_all(event_string):
if "name" in result.names:
self.name = escoria.utils.get_re_group(result, "name") \
.strip_edges()
if "flags" in result.names:
var _flags = escoria.utils.get_re_group(
result,
"flags"
).strip_edges().split(" ")
if "TK" in _flags:
self.flags |= FLAG_TK
if "NO_TT" in _flags:
self.flags |= FLAG_NO_TT
if "NO_HUD" in _flags:
self.flags |= FLAG_NO_HUD
if "NO_SAVE" in _flags:
self.flags |= FLAG_NO_SAVE
if "CUT_BLACK" in _flags:
self.flags |= FLAG_CUT_BLACK
if "LEAVE_BLACK" in _flags:
self.flags |= FLAG_LEAVE_BLACK
else:
escoria.logger.report_errors(
"Invalid event detected: %s" % event_string,
[
"Event regexp didn't match"
]
)
# Execute this statement and return its return code
func run() -> int:
escoria.logger.debug("Starting event %s" % name)
return .run()

View File

@@ -0,0 +1,11 @@
# Basic features and informations about ESC executions
extends Object
class_name ESCExecution
# Return codes handled by events
# * RC_OK: Event run okay
# * RC_CANCEL: Cancel all scheduled and queued events
# * RC_ERROR: Error running a command
# * RC_REPEAT: Repeat the current scope from the beginning
enum {RC_OK, RC_CANCEL, RC_ERROR, RC_REPEAT}

View File

@@ -0,0 +1,36 @@
# A group of ESC commands
extends ESCStatement
class_name ESCGroup
# A RegEx identifying a group
const REGEX = '^([^>]*)>\\s*(\\[(?<conditions>[^\\]]+)\\])?$'
# A list of ESCConditions to run this group
# Conditions are combined using logical AND
var conditions: Array = []
# Construct an ESC group of an ESC script line
func _init(group_string: String):
var group_regex = RegEx.new()
group_regex.compile(REGEX)
if group_regex.search(group_string):
for result in group_regex.search_all(group_string):
if "conditions" in result.names:
for condition in escoria.utils.get_re_group(
result,
"conditions"
).split(","):
self.conditions.append(
ESCCondition.new(condition.strip_edges())
)
else:
escoria.logger.report_errors(
"Invalid group detected: %s" % group_string,
[
"Group regexp didn't match"
]
)

View File

@@ -0,0 +1,72 @@
# An object handled in Escoria
extends Node
class_name ESCObject
# The global id of the object
var global_id: String
# Wether the object is active (visible to the player)
var active: bool = true setget _set_active
# Wether the object is interactive (clickable by the player)
var interactive: bool = true
# The state of the object. If the object has a respective animation,
# it will be played
var state: String = "default"
# The events registered with the object
var events: Dictionary = {}
# The node in the scene. Can be an ESCItem or an ESCCamera
var node: Node
func _init(p_global_id: String, p_node: Node):
global_id = p_global_id
node = p_node
# Set the state and start a possible animation
#
# #### Parameters
#
# - p_state: State to set
# - immediate: If true, skip directly to the end
func set_state(p_state: String, immediate: bool = false):
state = p_state
if node.has_method("get_animation_player"):
var animation_node = node.get_animation_player()
if animation_node:
animation_node.stop()
var actual_animator
if animation_node is AnimationPlayer:
if animation_node.has_animation(p_state):
if immediate:
animation_node.current_animation = p_state
animation_node.seek(
animation_node.get_animation(p_state).length
)
else:
animation_node.play(p_state)
elif animation_node is AnimatedSprite:
if animation_node.frames.has_animation(p_state):
if immediate:
animation_node.animation = p_state
animation_node.frame = \
animation_node.frames.get_frame_count(p_state)
else:
animation_node.play(p_state)
# Set the active value, thus hiding or showing the object
#
# #### Parameters
#
# - value: Value to set
func _set_active(value: bool):
active = value
self.node.visible = value

View File

@@ -0,0 +1,17 @@
# An event that is scheduled to run later
extends Object
class_name ESCScheduledEvent
# Event to run when timeout is reached
var event: ESCEvent
# The number of seconds until the event is run
var timeout: float
# Create a new scheduled event
func _init(p_event: ESCEvent, p_timeout: float):
self.event = p_event
self.timeout = p_timeout

View File

@@ -0,0 +1,7 @@
# A compiled ESC script
extends Object
class_name ESCScript
# A dictionary of ESCEvents in this script
var events: Dictionary

View File

@@ -0,0 +1,37 @@
# A statement in an ESC file
extends Object
class_name ESCStatement
# Emitted when the event did finish running
signal finished(return_code)
# The list of ESC commands
var statements: Array = []
# Check wether the statement should be run based on its conditions
func is_valid() -> bool:
for condition in self.conditions:
if not (condition as ESCCondition).run():
return false
return true
# Execute this statement and return its return code
func run() -> int:
var final_rc = ESCExecution.RC_OK
for statement in statements:
if statement.is_valid():
var rc = statement.run()
if rc is GDScriptFunctionState:
rc = yield(rc, "completed")
if rc == ESCExecution.RC_REPEAT:
return self.run()
elif rc != ESCExecution.RC_OK:
final_rc = rc
break
emit_signal("finished", final_rc)
return final_rc

View File

@@ -60,7 +60,6 @@ func _enter_tree():
add_child(area) add_child(area)
func _ready(): func _ready():
# escoria.register_object(self)
mouse_filter = MOUSE_FILTER_IGNORE mouse_filter = MOUSE_FILTER_IGNORE
area.connect("input_event", self, "manage_input") area.connect("input_event", self, "manage_input")
connect("gui_input", self, "manage_input_texturerect") connect("gui_input", self, "manage_input_texturerect")

View File

@@ -17,9 +17,6 @@ enum EDITOR_GAME_DEBUG_DISPLAY {
export(EDITOR_GAME_DEBUG_DISPLAY) var editor_debug_mode = EDITOR_GAME_DEBUG_DISPLAY.NONE setget set_editor_debug_mode export(EDITOR_GAME_DEBUG_DISPLAY) var editor_debug_mode = EDITOR_GAME_DEBUG_DISPLAY.NONE setget set_editor_debug_mode
func _ready():
escoria.esc_runner.connect("event_done", self, "_on_event_done")
func set_editor_debug_mode(p_editor_debug_mode : int) -> void: func set_editor_debug_mode(p_editor_debug_mode : int) -> void:
editor_debug_mode = p_editor_debug_mode editor_debug_mode = p_editor_debug_mode
@@ -104,10 +101,6 @@ func show_ui():
pass pass
## EVENTS
func _on_event_done(event_name: String):
pass
## FUNCTIONS BELOW THIS POINT DON'T NEED TO BE REIMPLEMENTED BY USER ## FUNCTIONS BELOW THIS POINT DON'T NEED TO BE REIMPLEMENTED BY USER
## (Although they can be, if required) ## (Although they can be, if required)

View File

@@ -2,9 +2,6 @@ tool
extends Area2D extends Area2D
class_name ESCItem class_name ESCItem
func get_class():
return "ESCItem"
""" """
ESCItem is a Sprite that defines an item, potentially interactive ESCItem is a Sprite that defines an item, potentially interactive
""" """
@@ -127,7 +124,13 @@ func _ready():
# Register and connect all elements to Escoria backoffice. # Register and connect all elements to Escoria backoffice.
if !Engine.is_editor_hint(): if !Engine.is_editor_hint():
escoria.register_object(self) escoria.object_manager.register_object(
ESCObject.new(
global_id,
self
),
true
)
terrain = escoria.room_terrain terrain = escoria.room_terrain
if !is_trigger: if !is_trigger:

View File

@@ -2,16 +2,6 @@ extends Node
const OBJ_DEFAULT_STATE = "default" const OBJ_DEFAULT_STATE = "default"
## Custom nodes:
#var ESCBackground = preload("res://addons/escoria-core/game/core-scripts/escbackground.gd")
#var ESCItem = preload("res://addons/escoria-core/game/core-scripts/escitem.gd")
#var ESCItemsInventory = preload("res://addons/escoria-core/game/core-scripts/items_inventory.gd")
#var ESCInventoryItem = preload("res://addons/escoria-core/game/core-scripts/inventory_item.gd")
#var ESCPlayer = preload("res://addons/escoria-core/game/core-scripts/escplayer.gd")
#var ESCRoom = preload("res://addons/escoria-core/game/core-scripts/escroom.gd")
#var ESCTerrain = preload("res://addons/escoria-core/game/core-scripts/escterrain.gd")
#var ESCTriggerZone = preload("res://addons/escoria-core/game/core-scripts/esctriggerzone.gd")
enum EVENT_LEVEL_STATE { enum EVENT_LEVEL_STATE {
RETURN, # 0 RETURN, # 0
YIELD, # 1 YIELD, # 1
@@ -20,65 +10,3 @@ enum EVENT_LEVEL_STATE {
CALL, # 4 CALL, # 4
JUMP # 5 JUMP # 5
} }
"""
ESCState is a helper class used to read ESC files. Once the ESC file is read and
decoded into ESCEvents and ESCCommands, the ESCState instance is removed.
"""
class ESCState:
var file # File or Dictionary
var line # String, can be null
var indent : int
var line_count : int
func _init(p_file, p_line, p_indent, p_line_count):
file = p_file
line = p_line
indent = p_indent
line_count = p_line_count
func _to_string():
return """ESCState: {
file: """ + file + """,
line: """ + line + """,
indent: """ + indent + """,
line_count: """ + line_count + """
}"""
class ESCEvent:
var ev_name : String
var ev_level : Array
var ev_flags : Array
func _init(p_name, p_level, p_flags):
ev_name = p_name
ev_level = p_level
ev_flags = p_flags
func _to_string():
return """ESCEvent: {
ev_name: """ + ev_name + """,
ev_level: """ + String(ev_level) + """,
ev_flags: """ + String(ev_flags) + """
}"""
class ESCCommand:
var name : String
var params : Array
var conditions : Dictionary
var flags : Array
func _init(p_name):
name = p_name
params = []
func _to_string():
return """ESCCommand: {
name: """ + name + """,
params: """ + String(params) + """,
conditions: """ + String(conditions) + """,
flags: """ + String(flags) + """
}"""

View File

@@ -82,7 +82,7 @@ func _ready():
# Connect the player to the event_done signal, so we can react to a finished # Connect the player to the event_done signal, so we can react to a finished
# ":setup" event. In this case, we need to run update_terrain() # ":setup" event. In this case, we need to run update_terrain()
escoria.esc_runner.connect("event_done", Movable, "update_terrain") escoria.event_manager.connect("event_finished", self, "update_terrain")
# assert(is_angle_in_interval(0, [340,40])) # true # assert(is_angle_in_interval(0, [340,40])) # true
# assert(is_angle_in_interval(359, [340,40])) # true # assert(is_angle_in_interval(359, [340,40])) # true
@@ -169,3 +169,5 @@ func set_angle(deg : int, immediate = true):
func set_speed(speed_value : int) -> void: func set_speed(speed_value : int) -> void:
speed = speed_value speed = speed_value
func update_terrain(rc: int, event_name: String) -> void:
Movable.update_terrain(event_name)

View File

@@ -39,11 +39,23 @@ func _ready():
if player_scene: if player_scene:
player = player_scene.instance() player = player_scene.instance()
add_child(player) add_child(player)
escoria.register_object(player) escoria.object_manager.register_object(
ESCObject.new(
player.global_id,
player
),
true
)
game.get_node("camera").set_target(player) game.get_node("camera").set_target(player)
if has_node("player_start"): if has_node("player_start"):
escoria.register_object($player_start) escoria.object_manager.register_object(
ESCObject.new(
$player_start.name,
$player_start
),
true
)
if global_id.empty(): if global_id.empty():
global_id = name global_id = name

View File

@@ -21,7 +21,7 @@ func _ready():
current_active_navigation_instance = n current_active_navigation_instance = n
if !Engine.is_editor_hint(): if !Engine.is_editor_hint():
escoria.register_object(self) escoria.room_terrain = self
#path = ImagePathFinder.new() #path = ImagePathFinder.new()
_update_texture() _update_texture()

View File

@@ -31,8 +31,9 @@ const ONE_LINE_HEIGHT = 16
func _ready(): func _ready():
escoria.call_deferred("register_object", self) if escoria.main.current_scene:
escoria.esc_runner.connect("action_changed", self, "on_action_selected") escoria.main.current_scene.game.tooltip_node = self
escoria.action_manager.connect("action_changed", self, "on_action_selected")
func set_color(p_color : Color): func set_color(p_color : Color):
@@ -62,7 +63,7 @@ func set_debug_mode(p_debug_mode : bool):
func on_action_selected() -> void: func on_action_selected() -> void:
current_action = escoria.esc_runner.current_action current_action = escoria.action_manager.current_action
update_tooltip_text() update_tooltip_text()

View File

@@ -1,32 +1,98 @@
# Logging # Logging framework for Escoria
onready var warning_is_reported : bool = false extends Object
class_name ESCLogger
# The path of the ESC file that was reported last (used for removing
# duplicate warnings
var warning_path : String var warning_path : String
func warning(string : String, args = []):
var argsstr = str(args) if !args.empty() else "" # Valid log levels
printerr("(W)\t" + string + " \t" + argsstr) enum { LOG_ERROR, LOG_WARNING, LOG_INFO, LOG_DEBUG }
# A map of log level names to log level ints
var _level_map: Dictionary = {
"ERROR": LOG_ERROR,
"WARNING": LOG_WARNING,
"INFO": LOG_INFO,
"DEBUG": LOG_DEBUG,
}
# Log a debug message
#
# #### Parameters
#
# * string: Text to log
# * args: Additional information
func debug(string : String, args = []):
if _get_log_level() >= LOG_DEBUG:
var argsstr = str(args) if !args.empty() else ""
printerr("(D)\t" + string + " \t" + argsstr)
# Log an info message
#
# #### Parameters
#
# * string: Text to log
# * args: Additional information
func info(string : String, args = []): func info(string : String, args = []):
var argsstr = [] if _get_log_level() >= LOG_INFO:
if !args.empty(): var argsstr = []
for arg in args: if !args.empty():
if arg is Array: for arg in args:
for p in arg: if arg is Array:
argsstr.append(p.global_id) for p in arg:
else: argsstr.append(p.global_id)
argsstr.append(str(arg)) else:
print("(I)\t" + string + " \t" + str(argsstr)) argsstr.append(str(arg))
print("(I)\t" + string + " \t" + str(argsstr))
# Log a warning message
#
# #### Parameters
#
# * string: Text to log
# * args: Additional information
func warning(string : String, args = []):
if _get_log_level() >= LOG_WARNING:
var argsstr = str(args) if !args.empty() else ""
printerr("(W)\t" + string + " \t" + argsstr)
if ProjectSettings.get_setting("escoria/debug/terminate_on_warnings"):
print_stack()
assert(false)
# Log an error message
#
# #### Parameters
#
# * string: Text to log
# * args: Additional information
func error(string : String, args = []): func error(string : String, args = []):
var argsstr = str(args) if !args.empty() else "" if _get_log_level() >= LOG_ERROR:
printerr("(E)\t" + string + " \t" + argsstr) var argsstr = str(args) if !args.empty() else ""
printerr("(E)\t" + string + " \t" + argsstr)
if ProjectSettings.get_setting("escoria/debug/terminate_on_errors"):
print_stack()
assert(false)
# Log a warning message about an ESC file
#
# #### Parameters
#
# * p_path: Path to the file
# * warnings: Array of warnings to put out
# * report_once: Additional messages about the same file will be ignored
func report_warnings(p_path : String, warnings : Array, report_once = false) -> void: func report_warnings(p_path : String, warnings : Array, report_once = false) -> void:
if p_path != warning_path: var warning_is_reported = false
warning_is_reported = false if p_path == warning_path:
warning_is_reported = true
if !warning_is_reported: if !warning_is_reported:
var text = "Warnings in file "+p_path+"\n" var text = "Warnings in file "+p_path+"\n"
@@ -41,6 +107,12 @@ func report_warnings(p_path : String, warnings : Array, report_once = false) ->
warning_is_reported = true warning_is_reported = true
# Log an error message about an ESC file
#
# #### Parameters
#
# * p_path: Path to the file
# * errors: Array of errors to put out
func report_errors(p_path : String, errors : Array) -> void: func report_errors(p_path : String, errors : Array) -> void:
var text = "Errors in file "+p_path+"\n" var text = "Errors in file "+p_path+"\n"
for e in errors: for e in errors:
@@ -49,8 +121,9 @@ func report_errors(p_path : String, errors : Array) -> void:
else: else:
text += e+"\n" text += e+"\n"
error(text) error(text)
if ProjectSettings.get_setting("escoria/debug/terminate_on_errors"):
print_stack()
assert(false) # Returns the currently set log level
# If your game stopped here, you may want to look at the Output tab and # **Returns** Log level as set in the configuration
# check for the error that caused the game to stop. func _get_log_level() -> int:
return _level_map[ProjectSettings.get_setting("escoria/debug/log_level")]

View File

@@ -1,10 +1,11 @@
# A cache for resources
extends Object
class_name ResourceCache
var thread : Thread var thread : Thread
var mutex : Mutex var mutex : Mutex
var sem : Semaphore var sem : Semaphore
#warning-ignore:unused_class_variable
var time_max = 100 # msec
var queue : Array = [] var queue : Array = []
var pending : Dictionary = {} var pending : Dictionary = {}
@@ -137,6 +138,13 @@ func get_resource(path):
return res return res
else: else:
_unlock("return") _unlock("return")
if not ProjectSettings.get_setting("escoria/platform/skip_cache"):
var res = ResourceLoader.load(path)
pending[path] = {
"res": res,
"permanent": true
}
return res
return ResourceLoader.load(path) return ResourceLoader.load(path)
func thread_process(): func thread_process():

View File

@@ -1,8 +0,0 @@
extends Script
class_name ESCScript
func get_class():
return "ESCScript"
func _init():
pass

View File

@@ -8,3 +8,59 @@ func _get_deg_from_rad(rad_angle : float):
deg = 0.0 deg = 0.0
return deg return deg
# Get the content of a reg exp group by name
#
# #### Parameters
#
# - re_match: The RegExMatch object
# - group: The name of the group
# **Returns** The value of the named regex group in the match
func get_re_group(re_match: RegExMatch, group: String) -> String:
if group in re_match.names:
return re_match.strings[re_match.names[group]]
else:
return ""
# Return a string value in the correct infered type
#
# #### Parameters
#
# - value: The original value
# **Returns** The typed value according to the type inference
func get_typed_value(value: String):
var regex_bool = RegEx.new()
regex_bool.compile("^true|false$")
var regex_float = RegEx.new()
regex_float.compile("^[0-9]+\\.[0-9]+$")
var regex_int = RegEx.new()
regex_int.compile("^[0-9]+$")
if regex_float.search(value):
return float(value)
elif regex_int.search(value):
return int(value)
elif regex_bool.search(value.to_lower()):
return true if value.to_lower() == "true" else false
else:
return str(value)
# Sanitize use of whitespaces in a string. Removes double whitespaces
# and converts tabs into space.
#
# #### Paramters
#
# - value: String to work on
# **Returns** the string with sanitized whitespaces
func sanitize_whitespace(value: String) -> String:
var tab_regex = RegEx.new()
tab_regex.compile("\\t")
var double_regex = RegEx.new()
double_regex.compile("\\s\\s+")
return double_regex.sub(
tab_regex.sub(value, " "),
" ",
true
)

View File

@@ -1,13 +1,36 @@
extends Node extends Node
# Scripts # Scripts
onready var esc_compiler = $esc_compiler
onready var logger = load("res://addons/escoria-core/game/core-scripts/log/logging.gd").new()
onready var main = $main onready var main = $main
onready var esc_runner = $esc_runner
onready var esc_level_runner = $esc_runner/esc_level_runner
onready var inputs_manager = $inputs_manager onready var inputs_manager = $inputs_manager
onready var utils = load("res://addons/escoria-core/game/core-scripts/utils/utils.gd").new() onready var utils = load("res://addons/escoria-core/game/core-scripts/utils/utils.gd").new()
onready var save_data = load("res://addons/escoria-core/game/core-scripts/save_data/save_data.gd").new()
# Logger used
var logger: ESCLogger
# The inventory manager instance
var inventory_manager: ESCInventoryManager
# The action manager instance
var action_manager: ESCActionManager
# ESC compiler instance
var esc_compiler: ESCCompiler
# ESC Event manager instance
var event_manager: ESCEventManager
# ESC globals registry instance
var globals_manager: ESCGlobalsManager
# ESC object manager instance
var object_manager: ESCObjectManager
# ESC command registry instance
var command_registry: ESCCommandRegistry
var resource_cache: ResourceCache
# INSTANCES # INSTANCES
var main_menu_instance var main_menu_instance
@@ -57,66 +80,46 @@ var settings_default : Dictionary = {
func _init(): func _init():
logger = load("res://addons/escoria-core/game/core-scripts/log/logging.gd").new() self.logger = ESCLogger.new()
self.inventory_manager = ESCInventoryManager.new()
self.action_manager = ESCActionManager.new()
self.event_manager = ESCEventManager.new()
self.globals_manager = ESCGlobalsManager.new()
self.add_child(self.event_manager)
self.object_manager = ESCObjectManager.new()
self.command_registry = ESCCommandRegistry.new()
self.esc_compiler = ESCCompiler.new()
self.resource_cache = ResourceCache.new()
self.resource_cache.start()
func _ready():
save_data.start()
save_data.check_settings()
var settings = save_data.load_settings(null)
escoria.settings = parse_json(settings)
escoria._on_settings_loaded(escoria.settings)
################################################################################## ##################################################################################
# Called by Main menu "start new game" # Called by Main menu "start new game"
func new_game(): func new_game():
var actions = esc_compiler.load_esc_file(ProjectSettings.get_setting("escoria/main/game_start_script")) var script = self.esc_compiler.load_esc_file(
$esc_runner.run_game(actions) ProjectSettings.get_setting("escoria/main/game_start_script")
)
event_manager.queue_event(script.events["start"])
var rc = yield(event_manager, "event_finished")
while rc[1] != "start":
rc = yield(event_manager, "event_finished")
escoria.main.scene_transition.fade_in() if rc[0] != ESCExecution.RC_OK:
yield(escoria.main.scene_transition, "transition_done") self.logger.report_errors(
"Start event of the start script returned unsuccessful: %d" % rc[0],
[]
)
""" return
Add object to the environement.
"""
func register_object(object : Object):
var object_id
if object.get("global_id"):
object_id = object.global_id
else:
object_id = object.name
if object is ESCDialogsPlayer:
dialog_player = object
if object is ESCPlayer:
$esc_runner.register_object(object_id, object, true)
if object is Position2D:
$esc_runner.register_object(object_id, object, true)
if object is ESCItem:
$esc_runner.register_object(object_id, object, true)
if object is ESCTerrain:
room_terrain = object
# if object is ESCBackground:
# $esc_runner.register_object(object_id, object, true)
if object is ESCCamera:
$esc_runner.register_object(object_id, object, true)
if object is ESCInventory:
inventory = object
if object is ESCTooltip:
if main.current_scene:
main.current_scene.game.tooltip_node = object
if object is ESCBackgroundMusic:
$esc_runner.register_object(object_id, object, true)
if object is ESCBackgroundSound:
$esc_runner.register_object(object_id, object, true)
""" """
Generic action function that runs an action on an element of the room (eg player walk) Generic action function that runs an action on an element of the room (eg player walk)
@@ -126,16 +129,21 @@ func do(action : String, params : Array = []) -> void:
if current_state == GAME_STATE.DEFAULT: if current_state == GAME_STATE.DEFAULT:
match action: match action:
"walk": "walk":
# Reset current action. self.action_manager.clear_current_action()
esc_runner.clear_current_action()
# Check moving object. # Check moving object.
if !escoria.esc_runner.check_obj(params[0], "escoria.do(walk)"): if not self.object_manager.has(params[0]):
escoria.logger.report_errors("escoria.gd:do()", self.logger.report_errors(
["Walk action requested on inexisting object: " + params[0]]) "escoria.gd:do()",
[
"Walk action requested on inexisting object: %s "\
% params[0]
]
)
return return
var moving_obj = escoria.esc_runner.get_object(params[0]) var moving_obj = self.object_manager.get_object(params[0])\
.node
# Walk to Position2D. # Walk to Position2D.
if params[1] is Vector2: if params[1] is Vector2:
@@ -148,14 +156,19 @@ func do(action : String, params : Array = []) -> void:
# Walk to object from its id # Walk to object from its id
elif params[1] is String: elif params[1] is String:
if !escoria.esc_runner.check_obj(params[1], "escoria.do(walk)"): if not self.object_manager.has(params[1]):
escoria.logger.report_errors("escoria.gd:do()", self.logger.report_errors(
["Walk action requested TOWARDS inexisting object: " + params[1]]) "escoria.gd:do()",
[
"Walk action requested TOWARDS " +\
"inexisting object: %s" % params[1]
]
)
return return
var object = escoria.esc_runner.get_object(params[1]) var object = self.object_manager.get_object(params[1])
if object: if object:
var target_position : Vector2 = object.interact_position var target_position : Vector2 = object.node.interact_position
var is_fast : bool = false var is_fast : bool = false
if params.size() > 2 and params[2] == true: if params.size() > 2 and params[2] == true:
is_fast = true is_fast = true
@@ -165,31 +178,40 @@ func do(action : String, params : Array = []) -> void:
"item_left_click": "item_left_click":
if params[0] is String: if params[0] is String:
escoria.logger.info("escoria.do() : item_left_click on item ", [params[0]]) self.logger.info("escoria.do() : item_left_click on item ", [params[0]])
var item = $esc_runner.get_object(params[0]) var item = self.object_manager.get_object(params[0])
ev_left_click_on_item(item, params[1]) ev_left_click_on_item(item, params[1])
"item_right_click": "item_right_click":
if params[0] is String: if params[0] is String:
escoria.logger.info("escoria.do() : item_right_click on item ", [params[0]]) self.logger.info("escoria.do() : item_right_click on item ", [params[0]])
ev_left_click_on_item($esc_runner.get_object(params[0]), params[1], true) var item = self.object_manager.get_object(params[0])
ev_left_click_on_item(item, params[1], true)
"trigger_in": "trigger_in":
var trigger_id = params[0] var trigger_id = params[0]
var object_id = params[1] var object_id = params[1]
var trigger_in_verb = params[2] var trigger_in_verb = params[2]
escoria.logger.info("escoria.do() : trigger_in " + trigger_id + " by " + object_id) self.logger.info("escoria.do() : trigger_in " + trigger_id + " by " + object_id)
esc_runner.run_event(esc_runner.objects_events_table[trigger_id][trigger_in_verb]) self.event_manager.queue_event(
object_manager.get_object(trigger_id).events[
trigger_in_verb
]
)
"trigger_out": "trigger_out":
var trigger_id = params[0] var trigger_id = params[0]
var object_id = params[1] var object_id = params[1]
var trigger_out_verb = params[2] var trigger_out_verb = params[2]
escoria.logger.info("escoria.do() : trigger_out " + trigger_id + " by " + object_id) self.logger.info("escoria.do() : trigger_out " + trigger_id + " by " + object_id)
esc_runner.run_event(esc_runner.objects_events_table[trigger_id][trigger_out_verb]) self.event_manager.queue_event(
object_manager.get_object(trigger_id).events[
trigger_out_verb
]
)
_: _:
escoria.logger.report_warnings("escoria.gd:do()", self.logger.report_warnings("escoria.gd:do()",
["Action received:", action, "with params ", params]) ["Action received:", action, "with params ", params])
elif current_state == GAME_STATE.WAIT: elif current_state == GAME_STATE.WAIT:
pass pass
@@ -204,25 +226,29 @@ func ev_left_click_on_item(obj, event, default_action = false):
event : event :
""" """
if obj is String: if obj is String:
obj = esc_runner.objects[obj] obj = object_manager.get_object(obj)
escoria.logger.info(obj.global_id + " left-clicked with event ", [event]) self.logger.info(obj.global_id + " left-clicked with event ", [event])
var need_combine = false var need_combine = false
# Check if current_action and current_tool are already set # Check if current_action and current_tool are already set
if esc_runner.current_action: if self.action_manager.current_action:
if esc_runner.current_tool: if self.action_manager.current_tool:
if esc_runner.current_action in esc_runner.current_tool.combine_if_action_used_among: if self.action_manager.current_action in self.action_manager\
.current_tool.node.combine_if_action_used_among:
need_combine = true need_combine = true
else: else:
esc_runner.current_tool = obj self.action_manager.current_tool = obj
else: else:
if default_action: if default_action:
if esc_runner.inventory_has(obj.global_id): if self.inventory_manager.inventory_has(obj.global_id):
esc_runner.current_action = obj.default_action_inventory self.action_manager.current_action = \
obj.node.default_action_inventory
else: else:
esc_runner.current_action = obj.default_action self.action_manager.current_action = \
elif esc_runner.current_action in obj.combine_if_action_used_among: obj.node.default_action
esc_runner.current_tool = obj elif self.action_manager.current_action in \
obj.node.combine_if_action_used_among:
self.action_manager.current_tool = obj
# Don't interact after player movement towards object (because object is inactive for example) # Don't interact after player movement towards object (because object is inactive for example)
@@ -230,33 +256,33 @@ func ev_left_click_on_item(obj, event, default_action = false):
var destination_position : Vector2 = main.current_scene.player.global_position var destination_position : Vector2 = main.current_scene.player.global_position
# Create walk context # Create walk context
var walk_context = {"fast": event.doubleclick, "target_object" : obj} var walk_context = {"fast": event.doubleclick, "target_object" : obj.node}
# If object not in inventory, player walks towards it # If object not in inventory, player walks towards it
if !esc_runner.inventory_has(obj.global_id): if not inventory_manager.inventory_has(obj.global_id):
var clicked_object_has_interact_position = false var clicked_object_has_interact_position = false
if esc_runner.get_interactive(obj.global_id): if object_manager.get_object(obj.global_id).interactive:
# if obj.interact_positions.default != null: # if obj.interact_positions.default != null:
# destination_position = obj.interact_positions.default#.global_position # destination_position = obj.interact_positions.default#.global_position
# clicked_object_has_interact_position = true # clicked_object_has_interact_position = true
# else: # else:
# destination_position = obj.position # destination_position = obj.position
if obj.get_interact_position() != null: if obj.node.get_interact_position() != null:
destination_position = obj.get_interact_position() destination_position = obj.node.get_interact_position()
clicked_object_has_interact_position = true clicked_object_has_interact_position = true
else: else:
destination_position = obj.position destination_position = obj.node.position
else: else:
destination_position = event.position destination_position = event.position
dont_interact = true dont_interact = true
# Use esc_runner for this? # Use ESC for this?
var is_already_walking = main.current_scene.player.walk_to(destination_position, walk_context) var is_already_walking = main.current_scene.player.walk_to(destination_position, walk_context)
# Wait for the player to arrive before continuing with action. # Wait for the player to arrive before continuing with action.
var context = yield(main.current_scene.player, "arrived") var context = yield(main.current_scene.player, "arrived")
escoria.logger.info("Context arrived: ", [context]) self.logger.info("Context arrived: ", [context])
if context.has("target_object") and walk_context.has("target_object"): if context.has("target_object") and walk_context.has("target_object"):
if (context.target_object.global_id != walk_context.target_object.global_id) \ if (context.target_object.global_id != walk_context.target_object.global_id) \
or (context.target_object.global_id == walk_context.target_object.global_id and is_already_walking): or (context.target_object.global_id == walk_context.target_object.global_id and is_already_walking):
@@ -276,24 +302,32 @@ func ev_left_click_on_item(obj, event, default_action = false):
# If player has arrived at the position he was supposed to reach so he can interact # If player has arrived at the position he was supposed to reach so he can interact
if player_global_pos == destination_position: if player_global_pos == destination_position:
# Manage exits # Manage exits
if obj.is_exit and $esc_runner.current_action == "" or $esc_runner.current_action == "walk": if obj.node.is_exit and self.action_manager.current_action == "" \
var params = [obj] or self.action_manager.current_action == "walk":
$esc_runner.activate("exit_scene", params) self.action_manager.activate("exit_scene", obj)
else: else:
# Manage movements towards object before activating it # Manage movements towards object before activating it
if $esc_runner.current_action == "" or $esc_runner.current_action == "walk": if self.action_manager.current_action == "" \
or self.action_manager.current_action == "walk":
if destination_position != clicked_position \ if destination_position != clicked_position \
and !esc_runner.inventory_has(obj.global_id): and not inventory_manager.inventory_has(obj.global_id):
esc_runner.activate("arrived", [obj]) self.action_manager.activate("arrived", obj)
# Manage action on object # Manage action on object
elif $esc_runner.current_action != "" and $esc_runner.current_action != "walk": elif self.action_manager.current_action != "" and \
self.action_manager.current_action != "walk":
# If apply_interact, perform combine between items # If apply_interact, perform combine between items
if need_combine: if need_combine:
esc_runner.activate(esc_runner.current_action, [esc_runner.current_tool, obj]) self.action_manager.activate(
self.action_manager.current_action,
self.action_manager.current_tool,
obj
)
else: else:
esc_runner.activate(esc_runner.current_action, [obj]) self.action_manager.activate(
self.action_manager.current_action,
obj
)
# else: # else:
## escoria.fallback("") ## escoria.fallback("")

View File

@@ -1,24 +1,12 @@
[gd_scene load_steps=7 format=2] [gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc/esc_runner.gd" type="Script" id=1]
[ext_resource path="res://addons/escoria-core/game/main.tscn" type="PackedScene" id=2] [ext_resource path="res://addons/escoria-core/game/main.tscn" type="PackedScene" id=2]
[ext_resource path="res://addons/escoria-core/game/escoria.gd" type="Script" id=3] [ext_resource path="res://addons/escoria-core/game/escoria.gd" type="Script" id=3]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc/esc_runner_level.gd" type="Script" id=4]
[ext_resource path="res://addons/escoria-core/game/inputs_manager.gd" type="Script" id=5] [ext_resource path="res://addons/escoria-core/game/inputs_manager.gd" type="Script" id=5]
[ext_resource path="res://addons/escoria-core/game/core-scripts/esc/esc_compiler.gd" type="Script" id=6]
[node name="escoria" type="Node"] [node name="escoria" type="Node"]
script = ExtResource( 3 ) script = ExtResource( 3 )
[node name="esc_compiler" type="Node" parent="."]
script = ExtResource( 6 )
[node name="esc_runner" type="Node" parent="."]
script = ExtResource( 1 )
[node name="esc_level_runner" type="Node" parent="esc_runner"]
script = ExtResource( 4 )
[node name="inputs_manager" type="Node" parent="."] [node name="inputs_manager" type="Node" parent="."]
script = ExtResource( 5 ) script = ExtResource( 5 )

View File

@@ -15,98 +15,36 @@ var screen_ofs = Vector2(0, 0)
onready var bg_music = $bg_music onready var bg_music = $bg_music
onready var scene_transition = $layers/curtain/scene_transition onready var scene_transition = $layers/curtain/scene_transition
func _ready():
$layers/wait_timer.connect("timeout", self, "_on_wait_finished")
func set_scene(p_scene, run_events=true): # Set the new current scene
""" #
Sets p_scene as current scene # #### Parameters
If run_events=true, plays the events defined in :setup event #
""" # - p_scene: Current scene to set
func set_scene(p_scene: Node):
if !p_scene: if !p_scene:
escoria.logger.report_errors("main", ["Trying to set empty scene"]) escoria.logger.report_errors("main", ["Trying to set empty scene"])
if current_scene != null: if current_scene != null:
clear_scene() clear_scene()
# get_node("/root").add_child(p_scene)
add_child(p_scene) add_child(p_scene)
move_child(p_scene, 0)
# Ensure we don't have a regular event running when changing scenes
if escoria.esc_runner.running_event:
assert(escoria.esc_runner.running_event.ev_name == "load")
var events : Dictionary = {}
if "esc_script" in p_scene and p_scene.esc_script and run_events:
events = escoria.esc_compiler.load_esc_file(p_scene.esc_script)
# # :setup is pretty much required in the code, but fortunately
# # we can help out with cases where one isn't necessary otherwise
# if not "setup" in events:
# var fake_setup = escoria.esc_compiler.compile_str(":setup\n")
# events["setup"] = fake_setup["setup"]
#
# escoria.esc_runner.run_event(events["setup"])
# # We need to ensure that :setup event is finished before adding the next event.
# var setup_done = false
# while !setup_done:
# var event_name = yield(escoria.esc_runner, "event_done")
# if event_name == "setup":
# setup_done = true
#
# # If scene was never visited, run "ready" event
# if !escoria.esc_runner.scenes_cache.has(p_scene.global_id) \
# and "ready" in events:
# escoria.esc_runner.run_event(events["ready"])
#
set_current_scene(p_scene, run_events)
set_camera_limits()
return events
func set_current_scene(p_scene, run_events=true):
current_scene = p_scene current_scene = p_scene
$"/root".move_child(current_scene, 0)
# Loading a save game must set the scene but not run events
if "events_path" in current_scene and current_scene.events_path and run_events:
if escoria.esc_runner.game:
# Having a game with `:setup` means we must wait for it to finish
if "setup" in escoria.esc_runner.game:
if not escoria.esc_runner.running_event:
escoria.logger.report_errors("main.gd:set_current_scene()", ["escoria.esc_runner.game has setup but no running_event"])
if escoria.esc_runner.running_event.ev_name != "setup":
escoria.logger.report_errors("main.gd:set_current_scene()", ["escoria.esc_runner.game has setup but it is not running: " + escoria.esc_runner.running_event.ev_name])
yield(escoria.esc_runner, "event_done")
else:
escoria.esc_compiler.load_file(current_scene.events_path)
# For a new game, we must run `:setup` if available
# and wait for it to finish
if "setup" in escoria.esc_runner.game:
escoria.esc_runner.run_event(escoria.esc_runner.game["setup"])
yield(escoria.esc_runner, "event_done")
if not escoria.esc_runner.running_event:
escoria.esc_runner.run_game()
escoria.esc_runner.register_object("_scene", p_scene, true) # Force overwrite of global
check_game_scene_methods() check_game_scene_methods()
set_camera_limits()
func clear_scene(): func clear_scene():
if current_scene == null: if current_scene == null:
return return
escoria.esc_runner.clear_current_action() escoria.action_manager.clear_current_action()
escoria.esc_runner.clear_current_tool() escoria.action_manager.clear_current_tool()
# escoria.esc_runner.hover_clear_stack()
# escoria.clear_inventory()
last_scene_global_id = current_scene.global_id remove_child(current_scene)
escoria.esc_runner.set_global("ESC_LAST_SCENE", last_scene_global_id, true)
get_node("/root").remove_child(current_scene)
current_scene.free() current_scene.free()
current_scene = null current_scene = null
@@ -116,9 +54,6 @@ func wait(params : Array, level):
$layers/wait_timer.set_one_shot(true) $layers/wait_timer.set_one_shot(true)
$layers/wait_timer.start() $layers/wait_timer.start()
func _on_wait_finished():
escoria.esc_level_runner.finished(wait_level)
func set_camera_limits(camera_limit_id : int = 0): func set_camera_limits(camera_limit_id : int = 0):
var limits = {} var limits = {}

View File

@@ -10,4 +10,3 @@ func _ready():
escoria.call_deferred("add_child", main_menu_scene) escoria.call_deferred("add_child", main_menu_scene)
escoria.main_menu_instance = main_menu_scene escoria.main_menu_instance = main_menu_scene

View File

@@ -185,5 +185,11 @@ func _ready():
target = Vector2(0, 0) target = Vector2(0, 0)
tween.connect("tween_completed", self, "target_reached") tween.connect("tween_completed", self, "target_reached")
escoria.register_object(self) escoria.object_manager.register_object(
ESCObject.new(
self.name,
self
),
true
)

View File

@@ -1,76 +1,108 @@
# Escoria dialog player
tool tool
extends ResourcePreloader extends ResourcePreloader
class_name ESCDialogsPlayer class_name ESCDialogsPlayer
func get_class():
return "ESCDialogsPlayer"
# This scene is in charge of ALL dialogs management : # Emitted when an answer as chosem
# - characters sayings #
# - player dialog options panel display/hiding and choices # ##### Parameters
#
# - option: The dialog option that was chosen
signal option_chosen(option)
var path_to_dialog_scenes : String # Emitted when a dialog line was finished
signal dialog_line_finished
# Wether the player is currently speaking
var is_speaking = false var is_speaking = false
var dialog_ui = null
var dialog_chooser_ui = null
# Reference to the dialog UI
var _dialog_ui = null
# Reference to the dialog chooser UI
var _dialog_chooser_ui = null
# Register the dialog player and load the dialog resources
func _ready(): func _ready():
if !Engine.is_editor_hint(): if !Engine.is_editor_hint():
escoria.register_object(self) escoria.dialog_player = self
preload_resources(ProjectSettings.get_setting("escoria/ui/dialogs_folder")) preload_resources(ProjectSettings.get_setting("escoria/ui/dialogs_folder"))
func preload_resources(path : String):
path_to_dialog_scenes = path
# Preload the dialog UI resources
#
# #### Parameters
#
# - path: Path where the actual dialog UI resources are located
func preload_resources(path: String) -> void:
var dialog_folder := Directory.new() var dialog_folder := Directory.new()
if !path_to_dialog_scenes.empty() and dialog_folder.open(path_to_dialog_scenes) == OK: if !path.empty() and dialog_folder.open(path) == OK:
dialog_folder.list_dir_begin() dialog_folder.list_dir_begin()
var file_name = dialog_folder.get_next() var file_name = dialog_folder.get_next()
while file_name != "": while file_name != "":
if !dialog_folder.current_is_dir() and file_name.get_extension() == "tscn": if !dialog_folder.current_is_dir() \
and file_name.get_extension() == "tscn":
var extension = "." + file_name.get_extension() var extension = "." + file_name.get_extension()
var basename = file_name.replace(extension, "") var basename = file_name.replace(extension, "")
if !has_resource(basename): if !has_resource(basename):
var file_path = dialog_folder.get_current_dir() + "/" + file_name var file_path = "%s/%s" % [
dialog_folder.get_current_dir(),
file_name
]
var dialog_scene = load(file_path) var dialog_scene = load(file_path)
if dialog_scene != null: if dialog_scene != null:
add_resource(basename, dialog_scene) add_resource(basename, dialog_scene)
file_name = dialog_folder.get_next() file_name = dialog_folder.get_next()
else: else:
escoria.logger.report_errors("dialog_player.gd:preload_resources()", escoria.logger.report_errors(
["An error occurred when trying to access the path: {_}.".format(path)]) "dialog_player.gd:preload_resources()",
[
"An error occurred when trying to access the path: %s." % path
]
)
func say(character : String, params : Dictionary): # A short one line dialog
#
# #### Parameters
#
# - character: Character that is talking
# - params: A dictionary of parameters. Currently only "line" is supported and
# holds the line the character should say
func say(character: String, params: Dictionary) -> void:
is_speaking = true is_speaking = true
dialog_ui = get_resource(params.ui).instance() _dialog_ui = get_resource(params.ui).instance()
get_parent().add_child(dialog_ui) get_parent().add_child(_dialog_ui)
dialog_ui.say(character, params) _dialog_ui.say(character, params)
yield(_dialog_ui, "dialog_line_finished")
emit_signal("dialog_line_finished")
func finish_fast(): # Called when a dialog line is skipped
dialog_ui.finish_fast() func finish_fast() -> void:
_dialog_ui.finish_fast()
# Options:
# type: (default value "default") the type of dialog menu to use. All types are in the "dd_player" scene.
# avatar: (default value "default") the avatar to use in the dialog ui.
# timeout: (default value 0) timeout to select an option. After the time has passed, the "timeout_option" will be selected automatically. If the value is 0, there's no timeout.
# timeout_option: (default value 0) option selected when timeout is reached.
func start_dialog_choices(answers : Array, options : Array):
if answers.empty():
escoria.logger.report_errors("dialog_player.gd:start_dialog_choices()", ["Received answers array was empty."])
dialog_chooser_ui = get_resource("text_dialog_choice").instance()
get_parent().add_child(dialog_chooser_ui)
dialog_chooser_ui.set_answers(answers)
func play_dialog_option_chosen(level_to_run : Array): # Display a list of choices
# escoria.esc_runner.finished(context) func start_dialog_choices(dialog: ESCDialog):
var ev_level = level_to_run if dialog.options.empty():
var ev = esctypes.ESCEvent.new("dialog_choice_done", ev_level, []) escoria.logger.report_errors(
escoria.esc_runner.add_level(ev, false) "dialog_player.gd:start_dialog_choices()",
dialog_chooser_ui.hide() ["Received answers array was empty."]
# stop() )
_dialog_chooser_ui = get_resource("text_dialog_choice").instance()
get_parent().add_child(_dialog_chooser_ui)
_dialog_chooser_ui.set_answers(dialog.options)
# Called when an option was chosen
func play_dialog_option_chosen(option: ESCDialogOption):
emit_signal("option_chosen", option)
_dialog_chooser_ui.hide()

View File

@@ -18,16 +18,18 @@ func _on_command_text_entered(p_command_str : String):
var actual_command = ":debug\n" + p_command_str + "\n" var actual_command = ":debug\n" + p_command_str + "\n"
var errors = [] var errors = []
var events = escoria.esc_compiler.compile_str(actual_command, errors) var script = escoria.esc_compiler.compile([
":debug",
p_command_str
])
if errors.empty(): if script:
#past_actions.text += str(events) escoria.event_manager.run(script.events["debug"])
var ret = escoria.esc_runner.run_event(events["debug"]) var ret = yield(escoria.event_manager, "event_finished")
if ret != null: while ret[1] != "debug":
past_actions.text += str(ret) ret = yield(escoria.event_manager, "event_finished")
else: if not ret[0] == ESCExecution.RC_OK:
# Display first error only past_actions.text += "Returned code: %d" % ret[0]
past_actions.text += str(errors[0].split(":")[1].strip_edges())
func _on_event_done(event_name : String): func _on_event_done(event_name : String):

View File

@@ -25,10 +25,10 @@ func _ready():
# if !Engine.is_editor_hint(): # if !Engine.is_editor_hint():
# return # return
for item_id in escoria.esc_runner.items_in_inventory(): for item_id in escoria.inventory_manager.items_in_inventory():
call_deferred("add_new_item_by_id", item_id) call_deferred("add_new_item_by_id", item_id)
escoria.register_object(self) escoria.inventory = self
if inventory_ui_container == null or inventory_ui_container.is_empty(): if inventory_ui_container == null or inventory_ui_container.is_empty():
escoria.logger.report_errors(self.get_path(), ["Items container is empty."]) escoria.logger.report_errors(self.get_path(), ["Items container is empty."])
@@ -37,29 +37,44 @@ func _ready():
items_ids_in_inventory[c.item_id] = c items_ids_in_inventory[c.item_id] = c
# c.connect("pressed", escoria.inputs_manager, "_on_inventory_item_pressed", [c.item_id]) # c.connect("pressed", escoria.inputs_manager, "_on_inventory_item_pressed", [c.item_id])
escoria.esc_runner.connect("global_changed", self, "_on_escoria_global_changed") escoria.globals_manager.connect("global_changed", self, "_on_escoria_global_changed")
# add item to Inventory UI using its id set in its scene # add item to Inventory UI using its id set in its scene
func add_new_item_by_id(item_id : String) -> void: func add_new_item_by_id(item_id : String) -> void:
if item_id.begins_with("i/"): if item_id.begins_with("i/"):
item_id = item_id.rsplit("i/", false)[0] item_id = item_id.rsplit("i/", false)[0]
if !items_ids_in_inventory.has(item_id): if not items_ids_in_inventory.has(item_id):
if !escoria.esc_runner.check_obj(item_id, "add_new_item_by_id"): if not escoria.object_manager.has(item_id):
escoria.logger.report_errors("inventory_ui.gd:add_new_item_by_id()", escoria.logger.report_errors(
["Item global id '"+ item_id + "' does not exist.", "inventory_ui.gd:add_new_item_by_id()",
"Check item's id in ESCORIA_ALL_ITEMS scene."]) [
if !all_items.get_inventory_item(item_id): "Item global id '%s' does not exist." % item_id,
escoria.logger.report_errors("inventory_ui.gd:add_new_item_by_id()", "Check item's id in ESCORIA_ALL_ITEMS scene."
["Item global id '"+ item_id + "' doesn't have corresponding inventory item.", ]
"Check item's id in ESCORIA_ALL_ITEMS scene."]) )
if not all_items.get_inventory_item(item_id):
escoria.logger.report_errors(
"inventory_ui.gd:add_new_item_by_id()",
[
"Item global id '%s' doesn't have a " +\
"corresponding inventory item." % item_id,
"Check item's id in ESCORIA_ALL_ITEMS scene."
]
)
var item_inventory_button = all_items.get_inventory_item(item_id).duplicate() var item_inventory_button = all_items.get_inventory_item(item_id).duplicate()
items_ids_in_inventory[item_id] = item_inventory_button items_ids_in_inventory[item_id] = item_inventory_button
get_node(inventory_ui_container).add_item(item_inventory_button) get_node(inventory_ui_container).add_item(item_inventory_button)
# Add the item to inventory # Add the item to inventory
if !escoria.esc_runner.objects.has(item_id): if not escoria.object_manager.has(item_id):
escoria.esc_runner.register_object(item_id, item_inventory_button) escoria.object_manager.register_object(
ESCObject.new(
item_id,
item_inventory_button
),
true
)
item_inventory_button.visible = true item_inventory_button.visible = true
item_inventory_button.connect("mouse_left_inventory_item", item_inventory_button.connect("mouse_left_inventory_item",
@@ -94,17 +109,14 @@ func remove_item_by_id(item_id : String) -> void:
item_inventory_button.queue_free() item_inventory_button.queue_free()
items_ids_in_inventory.erase(item_id) items_ids_in_inventory.erase(item_id)
func _on_escoria_global_changed(global : String) -> void: func _on_escoria_global_changed(global : String, old_value, new_value) -> void:
if !global.begins_with("i/"): if !global.begins_with("i/"):
return return
var item = global.rsplit("i/", false) var item = global.rsplit("i/", false)
if item.size() == 1: if item.size() == 1:
if escoria.esc_runner.globals[global] == "true": if new_value:
add_new_item_by_id(item[0]) add_new_item_by_id(item[0])
elif escoria.esc_runner.globals[global] == "false":
remove_item_by_id(item[0])
else: else:
escoria.logger.report_warnings("inventory_ui.gd:_on_escoria_global_changed()", \ remove_item_by_id(item[0])
["Inventory global " + global + " is neither 'true' nor 'false' (was " + escoria.esc_runner.globals[global] + "). "])
else: else:
escoria.logger.report_errors("inventory_ui.gd:_on_escoria_global_changed()", ["Global must contain 1 item name.", "(received: " + global + ")"]) escoria.logger.report_errors("inventory_ui.gd:_on_escoria_global_changed()", ["Global must contain 1 item name.", "(received: " + global + ")"])

View File

@@ -11,8 +11,10 @@ export var global_id = "bg_music"
func game_cleared(): func game_cleared():
set_state("off", true) set_state("off", true)
self.disconnect("tree_exited", escoria.esc_runner, "object_exit_scene") escoria.object_manager.register_object(
escoria.register_object(self) ESCObject.new(global_id, self),
true
)
func set_state(p_state, p_force = false): func set_state(p_state, p_force = false):
@@ -38,5 +40,8 @@ func set_state(p_state, p_force = false):
stream.play() stream.play()
func _ready(): func _ready():
escoria.register_object(self) escoria.object_manager.register_object(
ESCObject.new(global_id, self),
true
)

View File

@@ -11,7 +11,10 @@ export var global_id = "bg_sound"
func game_cleared(): func game_cleared():
stream.stream = null stream.stream = null
escoria.register_object(self) escoria.object_manager.register_object(
ESCObject.new(global_id, self),
true
)
func set_state(p_state, p_force = false): func set_state(p_state, p_force = false):
@@ -38,4 +41,7 @@ func set_state(p_state, p_force = false):
func _ready(): func _ready():
escoria.register_object(self) escoria.object_manager.register_object(
ESCObject.new(global_id, self),
true
)

View File

@@ -1,6 +1,7 @@
tool tool
extends PanelContainer extends PanelContainer
signal dialog_line_started
signal dialog_line_finished signal dialog_line_finished
export(String) var current_character setget set_current_character export(String) var current_character setget set_current_character
@@ -40,6 +41,7 @@ params: Dictionary
""" """
func say(character : String, params : Dictionary) : func say(character : String, params : Dictionary) :
show() show()
emit_signal("dialog_line_started")
set_current_character(character) set_current_character(character)
if !params["line"]: if !params["line"]:
@@ -70,10 +72,8 @@ func _on_dialog_line_typed(object, key):
$Timer.connect("timeout", self, "_on_dialog_finished") $Timer.connect("timeout", self, "_on_dialog_finished")
func _on_dialog_finished(): func _on_dialog_finished():
escoria.esc_level_runner.finished()
escoria.dialog_player.is_speaking = false
escoria.current_state = escoria.GAME_STATE.DEFAULT escoria.current_state = escoria.GAME_STATE.DEFAULT
# emit_signal("dialog_line_finished") emit_signal("dialog_line_finished")
queue_free() queue_free()

View File

@@ -1,10 +0,0 @@
extends Control
func _ready():
pass
func set_answers(answers_array : PoolStringArray):
func _on_answer1_gui_input(event):
pass # Replace with function body.

View File

@@ -12,12 +12,11 @@ func _ready():
c.queue_free() c.queue_free()
func set_answers(p_commands : Array): func set_answers(options : Array):
commands = p_commands commands = options
var c = 0
for option in commands: for option in commands:
var new_answer_label = RichTextLabel.new() var new_answer_label = RichTextLabel.new()
new_answer_label.text = option.params[0] new_answer_label.text = option.option
new_answer_label.fit_content_height = true new_answer_label.fit_content_height = true
new_answer_label.add_font_override("normal_font", font) new_answer_label.add_font_override("normal_font", font)
@@ -27,13 +26,10 @@ func set_answers(p_commands : Array):
new_answer_label.connect("focus_exited", self, "_on_answer_focus_exited", [new_answer_label]) # Focus exited new_answer_label.connect("focus_exited", self, "_on_answer_focus_exited", [new_answer_label]) # Focus exited
new_answer_label.connect("mouse_entered", self, "_on_answer_mouse_entered", [new_answer_label]) # Mouse entered new_answer_label.connect("mouse_entered", self, "_on_answer_mouse_entered", [new_answer_label]) # Mouse entered
new_answer_label.connect("mouse_exited", self, "_on_answer_mouse_exited", [new_answer_label]) # Mouse exited new_answer_label.connect("mouse_exited", self, "_on_answer_mouse_exited", [new_answer_label]) # Mouse exited
new_answer_label.connect("gui_input", self, "_on_answer_gui_input", [c]) # Clicks new_answer_label.connect("gui_input", self, "_on_answer_gui_input", [option]) # Clicks
c += 1
func _on_answer_gui_input(event : InputEvent, answer_id : int): func _on_answer_gui_input(event : InputEvent, answer : ESCDialogOption):
if event is InputEventMouseButton and event.is_pressed(): if event is InputEventMouseButton and event.is_pressed():
var answer = commands[answer_id].params[1]
printt(answer)
escoria.dialog_player.play_dialog_option_chosen(answer) escoria.dialog_player.play_dialog_option_chosen(answer)
func _on_answer_mouse_entered(answer_node : Node): func _on_answer_mouse_entered(answer_node : Node):

View File

@@ -1,7 +1,7 @@
extends RichTextLabel extends RichTextLabel
#signal dialog_line_started signal dialog_line_started
#signal dialog_line_finished signal dialog_line_finished
onready var tween = $Tween onready var tween = $Tween
onready var text_node = self onready var text_node = self
@@ -28,12 +28,14 @@ params: Dictionary
func say(character : String, params : Dictionary) : func say(character : String, params : Dictionary) :
show() show()
emit_signal("dialog_line_started")
if !params["line"]: if !params["line"]:
escoria.logger.report_errors("dialog_box_inset.gd:say()", ["No line field in params!"]) escoria.logger.report_errors("dialog_box_inset.gd:say()", ["No line field in params!"])
return return
# Position the RichTextLabel on the character's dialog position, if any. # Position the RichTextLabel on the character's dialog position, if any.
current_character = escoria.esc_runner.get_object(character) current_character = escoria.object_manager.get_object(character).node
rect_position = current_character.get_node("dialog_position").get_global_transform_with_canvas().origin rect_position = current_character.get_node("dialog_position").get_global_transform_with_canvas().origin
rect_position.x -= rect_size.x / 2 rect_position.x -= rect_size.x / 2
@@ -73,7 +75,7 @@ func _on_dialog_line_typed(object, key):
func _on_dialog_finished(): func _on_dialog_finished():
current_character.stop_talking() current_character.stop_talking()
escoria.esc_level_runner.finished() emit_signal("dialog_line_finished")
escoria.dialog_player.is_speaking = false escoria.dialog_player.is_speaking = false
escoria.current_state = escoria.GAME_STATE.DEFAULT escoria.current_state = escoria.GAME_STATE.DEFAULT
queue_free() queue_free()

View File

@@ -3,7 +3,7 @@ say player "It's a bottle."
stop stop
:pickup :pickup
inventory_add r9_bottle true inventory_add r9_bottle
set_active r9_bottle_left false set_active r9_bottle_left false
set_active r9_bottle_middle false set_active r9_bottle_middle false
set_active r9_bottle_right false set_active r9_bottle_right false

View File

@@ -4,7 +4,7 @@
# Set player look left # Set player look left
set_angle player 270 set_angle player 270
stop stop
> [!last_scene] > [!eq ESC_LAST_SCENE room2]
teleport player player_start teleport player player_start
stop stop

View File

@@ -1,13 +1,13 @@
:look :look
> [! dialog_advance] > [! dialog_advance]
say player ROOM1_look_wall_item_1:"I don't know what that stuff is." say player "I don't know what that stuff is."
set_global dialog_advance 1 set_global dialog_advance 1
stop stop
> [eq dialog_advance 1] > [eq dialog_advance 1]
say player ROOM1_look_wall_item_2:"I REALLY don't know what that stuff is." say player "I REALLY don't know what that stuff is."
set_global dialog_advance 2 set_global dialog_advance 2
stop stop
> [eq dialog_advance 2] > [eq dialog_advance 2]
say player ROOM1_look_wall_item_3:"No, SERIOUSLY, I have no idea what that is!" say player "No, SERIOUSLY, I have no idea what that is!"
say player ROOM1_look_wall_item_4:"Please stop asking me that!" say player "Please stop asking me that!"
stop stop

Some files were not shown because too many files have changed in this diff Show More