diff options
| author | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
|---|---|---|
| committer | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
| commit | a89e744ed0e8c759caf041a580112365bbfe4303 (patch) | |
| tree | dd61e320904176995727e86a857d00c90d4933ba /addons/escoria-core/game/core-scripts | |
| parent | 6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff) | |
Diffstat (limited to 'addons/escoria-core/game/core-scripts')
132 files changed, 4399 insertions, 717 deletions
diff --git a/addons/escoria-core/game/core-scripts/behaviors/esc_movable.gd b/addons/escoria-core/game/core-scripts/behaviors/esc_movable.gd index 27ed7a5..aae7837 100644 --- a/addons/escoria-core/game/core-scripts/behaviors/esc_movable.gd +++ b/addons/escoria-core/game/core-scripts/behaviors/esc_movable.gd @@ -1,7 +1,7 @@ ## Node that performs the moving (walk, teleport, terrain scaling...) actions on ## its parent node. -extends Node class_name ESCMovable +extends Node ## Tasks carried out by this walkable node[br] @@ -623,5 +623,4 @@ func get_shortest_way_to_dir(current_dir: int, target_dir: int) -> int: if internal and current_dir < target_dir or \ (not internal and current_dir > target_dir): return 1 - else: - return -1 + return -1 diff --git a/addons/escoria-core/game/core-scripts/esc/commands/accept_input.gd b/addons/escoria-core/game/core-scripts/esc/commands/accept_input.gd index f29f9c7..18a6228 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/accept_input.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/accept_input.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name AcceptInputCommand +extends ESCBaseCommand ## The list of supported input types diff --git a/addons/escoria-core/game/core-scripts/esc/commands/anim.gd b/addons/escoria-core/game/core-scripts/esc/commands/anim.gd index 6525654..5711707 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/anim.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/anim.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name AnimCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd index b64b166..f564a94 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name AnimBlockCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/block_say.gd b/addons/escoria-core/game/core-scripts/esc/commands/block_say.gd index d2b7dd2..6ebb0c1 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/block_say.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/block_say.gd @@ -8,8 +8,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name BlockSayCommand +extends ESCBaseCommand ## Constructor[br] @@ -48,7 +48,7 @@ func configure() -> ESCCommandArgumentDescriptor: ## #### Returns[br] ## [br] ## Returns True if the arguments are valid, false otherwise. (`bool`) -func validate(arguments: Array): +func validate(_arguments: Array): return true @@ -63,7 +63,7 @@ func validate(arguments: Array): ## #### Returns[br] ## [br] ## Returns the execution result code. (`int`) -func run(command_params: Array) -> int: +func run(_command_params: Array) -> int: escoria.dialog_player.enable_preserve_dialog_box() return ESCExecution.RC_OK diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_push.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_push.gd index 37f99dd..710a195 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_push.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_push.gd @@ -26,8 +26,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraPushCommand +extends ESCCameraBaseCommand ## The list of supported transitions as per the link mentioned above const SUPPORTED_TRANSITIONS = ["LINEAR","SINE","QUINT","QUART","QUAD" ,"EXPO","ELASTIC","CUBIC", diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_push_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_push_block.gd index 289cfc7..52db676 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_push_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_push_block.gd @@ -26,8 +26,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraPushBlockCommand +extends ESCCameraBaseCommand ## The list of supported transitions as per the link mentioned above diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_limits.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_limits.gd index 79dc992..b468e7d 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_limits.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_limits.gd @@ -15,8 +15,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetLimitsCommand +extends ESCCameraBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos.gd index 5403bcf..365b668 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos.gd @@ -12,8 +12,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetPosCommand +extends ESCCameraBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos_block.gd index 2fe7dc0..8833c24 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos_block.gd @@ -15,8 +15,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetPosBlockCommand +extends ESCCameraBaseCommand # Tween for blocking diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target.gd index 8d5e3e0..485dd28 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target.gd @@ -13,8 +13,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetTargetCommand +extends ESCCameraBaseCommand ## Provides the argument descriptor that defines the command signature.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target_block.gd index e071dec..07841fd 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_target_block.gd @@ -17,8 +17,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetTargetBlockCommand +extends ESCCameraBaseCommand # Tween for blocking diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom.gd index 515aa44..4be55ee 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom.gd @@ -16,8 +16,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetZoomCommand +extends ESCCameraBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_block.gd index 07ea05f..4ae84b8 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_block.gd @@ -20,8 +20,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraSetZoomBlockCommand +extends ESCCameraBaseCommand var _camera_tween: Tween3 diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height.gd index 5ffa7c0..dba9ad5 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name CameraSetZoomHeightCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height_block.gd index b526d9c..429be97 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height_block.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name CameraSetZoomHeightBlockCommand +extends ESCBaseCommand ## Tween for blocking diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_shift.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_shift.gd index fb95bb0..379b4f3 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_shift.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_shift.gd @@ -20,8 +20,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraShiftCommand +extends ESCCameraBaseCommand ## The list of supported transitions as per the link mentioned above const SUPPORTED_TRANSITIONS = ["LINEAR","SINE","QUINT","QUART","QUAD" ,"EXPO","ELASTIC","CUBIC", diff --git a/addons/escoria-core/game/core-scripts/esc/commands/camera_shift_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/camera_shift_block.gd index a06a8db..c7294a3 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/camera_shift_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/camera_shift_block.gd @@ -23,8 +23,8 @@ ## ## @ASHES ## @COMMAND -extends ESCCameraBaseCommand class_name CameraShiftBlockCommand +extends ESCCameraBaseCommand # The list of supported transitions as per the link mentioned above diff --git a/addons/escoria-core/game/core-scripts/esc/commands/change_scene.gd b/addons/escoria-core/game/core-scripts/esc/commands/change_scene.gd index 2ae6e23..b1ff671 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/change_scene.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/change_scene.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name ChangeSceneCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/change_scene_godot.gd b/addons/escoria-core/game/core-scripts/esc/commands/change_scene_godot.gd index 9be93a8..e7b8b70 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/change_scene_godot.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/change_scene_godot.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name ChangeSceneGodotCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/custom.gd b/addons/escoria-core/game/core-scripts/esc/commands/custom.gd index afacce8..c2fe35f 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/custom.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/custom.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name CustomCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -54,7 +54,7 @@ func validate(arguments: Array): if not escoria.object_manager.has(arguments[0]): raise_invalid_object_error(self, arguments[0]) return false - elif not escoria.object_manager.get_object(arguments[0]).node.has_node( + if not escoria.object_manager.get_object(arguments[0]).node.has_node( arguments[1] ): raise_error(self, "Invalid node. Object with global id %s has no child node called %s." @@ -63,7 +63,7 @@ func validate(arguments: Array): arguments[1], ]) return false - elif not escoria.object_manager.get_object(arguments[0]).node\ + if not escoria.object_manager.get_object(arguments[0]).node\ .get_node( arguments[1] )\ diff --git a/addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd b/addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd index 8f91433..33aa173 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name DecGlobalCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/enable_terrain.gd b/addons/escoria-core/game/core-scripts/esc/commands/enable_terrain.gd index 106c297..76b3fe9 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/enable_terrain.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/enable_terrain.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name EnableTerrainCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -52,9 +52,8 @@ func run(command_params: Array) -> int: new_active_navigation_instance escoria.room_terrain.current_active_navigation_instance.enabled = true return ESCExecution.RC_OK - else: - raise_error(self, "Can not find terrain node. Terrain node %s could not be found." % name) - return ESCExecution.RC_ERROR + raise_error(self, "Can not find terrain node. Terrain node %s could not be found." % name) + return ESCExecution.RC_ERROR ## Function called when the command is interrupted.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/end_block_say.gd b/addons/escoria-core/game/core-scripts/esc/commands/end_block_say.gd index 9a68f4f..d940ce9 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/end_block_say.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/end_block_say.gd @@ -8,8 +8,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name EndBlockSayCommand +extends ESCBaseCommand ## Constructor (bypasses the default constructor to avoid)[br] @@ -49,7 +49,7 @@ func configure() -> ESCCommandArgumentDescriptor: ## #### Returns[br] ## [br] ## Returns True if the arguments are valid, false otherwise. (`bool`) -func validate(arguments: Array): +func validate(_arguments: Array): return true @@ -64,7 +64,7 @@ func validate(arguments: Array): ## #### Returns[br] ## [br] ## Returns the execution result code. (`int`) -func run(command_params: Array) -> int: +func run(_command_params: Array) -> int: escoria.dialog_player.disable_preserve_dialog_box() return ESCExecution.RC_OK diff --git a/addons/escoria-core/game/core-scripts/esc/commands/hide_menu.gd b/addons/escoria-core/game/core-scripts/esc/commands/hide_menu.gd index 65567d7..da23186 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/hide_menu.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/hide_menu.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name HideMenuCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -69,7 +69,7 @@ func run(command_params: Array) -> int: # Transition out from menu transition_id = escoria.main.scene_transition.transition( "", - ESCTransitionPlayer.TRANSITION_MODE.OUT + ESCTransitionPlayer.TransitionMode.OUT ) if transition_id != ESCTransitionPlayer.TRANSITION_ID_INSTANT: diff --git a/addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd b/addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd index 2bacd61..2452dcc 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name IncGlobalCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/inventory_add.gd b/addons/escoria-core/game/core-scripts/esc/commands/inventory_add.gd index d715dea..ba59d17 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/inventory_add.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/inventory_add.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name InventoryAddCommand +extends ESCBaseCommand ## List of illegal strings that cannot be used in item names. diff --git a/addons/escoria-core/game/core-scripts/esc/commands/inventory_remove.gd b/addons/escoria-core/game/core-scripts/esc/commands/inventory_remove.gd index bd45f66..93e4bab 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/inventory_remove.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/inventory_remove.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name InventoryRemoveCommand +extends ESCBaseCommand ## List of illegal strings that cannot be used in item names. const ILLEGAL_STRINGS = ["/"] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd b/addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd index 90dcc94..41321d8 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd @@ -12,14 +12,27 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name PlaySndCommand +extends ESCBaseCommand ## The specified sound player var _snd_player: String +## Constructor of this command.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _init() -> void: + run_in_ready_allowed_during_savegame_loading = false + + ## The descriptor of the arguments of this command.[br] ## [br] ## #### Parameters[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/print_internal.gd b/addons/escoria-core/game/core-scripts/esc/commands/print_internal.gd index 56a601f..418cc72 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/print_internal.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/print_internal.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name PrintCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd b/addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd index f676fda..bad3a19 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name QueueEventCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -64,7 +64,7 @@ func validate(arguments: Array): var esc_script = escoria.esc_compiler.load_esc_file(node.esc_script) - if not arguments[1] in esc_script.events: + if not esc_script.has_event_with_target(arguments[1]): raise_error( self, "Event with name '%s' not found." % arguments[1] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd b/addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd index 845ae1c..c0598d2 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name QueueResourceCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd b/addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd index a165f8d..9db0c20 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name RandGlobalCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/repeat.gd b/addons/escoria-core/game/core-scripts/esc/commands/repeat.gd index 0c0e4b6..db891bf 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/repeat.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/repeat.gd @@ -8,8 +8,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name RepeatCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -40,7 +40,7 @@ func configure() -> ESCCommandArgumentDescriptor: ## #### Returns[br] ## [br] ## Returns the execution result code. (`int`) -func run(command_params: Array) -> int: +func run(_command_params: Array) -> int: return ESCExecution.RC_CANCEL diff --git a/addons/escoria-core/game/core-scripts/esc/commands/save_game.gd b/addons/escoria-core/game/core-scripts/esc/commands/save_game.gd index e830c49..9f66993 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/save_game.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/save_game.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SaveGameCommand +extends ESCBaseCommand ## Constructor[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/say.gd b/addons/escoria-core/game/core-scripts/esc/commands/say.gd index 8c043c2..75115c5 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/say.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/say.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SayCommand +extends ESCBaseCommand ## The keyword used to refer to the current player const CURRENT_PLAYER_KEYWORD = "CURRENT_PLAYER" @@ -103,14 +103,14 @@ func validate(arguments: Array): func run(command_params: Array) -> int: var dict: Dictionary - escoria.current_state = escoria.GAME_STATE.DIALOG + escoria.current_state = escoria.GameState.DIALOG if !escoria.dialog_player: raise_error( self, "No dialog player was registered and the 'say' command was encountered." ) - escoria.current_state = escoria.GAME_STATE.DEFAULT + escoria.current_state = escoria.GameState.DEFAULT return ESCExecution.RC_ERROR if not escoria.main.current_scene.player: @@ -119,7 +119,7 @@ func run(command_params: Array) -> int: "[%s]: No player item in the current scene was registered and the say command was encountered." % get_command_name() ) - escoria.current_state = escoria.GAME_STATE.DEFAULT + escoria.current_state = escoria.GameState.DEFAULT return ESCExecution.RC_CANCEL # Replace the names of any globals in "{ }" with their value @@ -136,7 +136,7 @@ func run(command_params: Array) -> int: command_params[2] ) await escoria.dialog_player.say_finished - escoria.current_state = escoria.GAME_STATE.DEFAULT + escoria.current_state = escoria.GameState.DEFAULT return ESCExecution.RC_OK diff --git a/addons/escoria-core/game/core-scripts/esc/commands/sched_event.gd b/addons/escoria-core/game/core-scripts/esc/commands/sched_event.gd index 5b57dca..3fb2029 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/sched_event.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/sched_event.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SchedEventCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -63,7 +63,7 @@ func validate(arguments: Array): var esc_script = escoria.esc_compiler.load_esc_file(node.esc_script) - if not arguments[2] in esc_script.events: + if not esc_script.has_event_with_target(arguments[2]): raise_error( self, "Event with name '%s' not found." % arguments[2] @@ -104,7 +104,7 @@ func run(command_params: Array) -> int: var esc_script = escoria.esc_compiler.load_esc_file(node.esc_script) escoria.event_manager.schedule_event( - esc_script.events[command_params[2]], # ESCGrammarStmts.Event + esc_script.get_event_with_target(command_params[2]), # ESCGrammarStmts.Event command_params[0], # timeout float command_params[1] # object string ) diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_active.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_active.gd index 90846e5..97ebee5 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_active.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_active.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetActiveCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_active_if_exists.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_active_if_exists.gd index ebb02cb..a2303fb 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_active_if_exists.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_active_if_exists.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetActiveIfExistsCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_angle.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_angle.gd index ef503c4..936b03a 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_angle.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_angle.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetAngleCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_animations.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_animations.gd index 7ae818f..9c22c02 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_animations.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_animations.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetAnimationsCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -57,7 +57,7 @@ func validate(arguments: Array): ) return false - (escoria.object_manager.get_object(arguments[0]).node as ESCPlayer).validate_animations(load(arguments[1])) + (escoria.object_manager.get_object(arguments[0]).node as ESCItem).validate_animations(load(arguments[1])) return true @@ -74,7 +74,7 @@ func validate(arguments: Array): ## [br] ## Returns the execution result code. (`int`) func run(command_params: Array) -> int: - (escoria.object_manager.get_object(command_params[0]).node as ESCPlayer)\ + (escoria.object_manager.get_object(command_params[0]).node as ESCItem)\ .animations = load(command_params[1]) if not escoria.globals_manager.has( escoria.room_manager.GLOBAL_ANIMATION_RESOURCES @@ -89,7 +89,7 @@ func run(command_params: Array) -> int: ) if animations.is_empty(): animations = {} - animations[command_params[0]] = command_params[1] + animations[command_params[0]] = command_params[1] escoria.globals_manager.set_global( escoria.room_manager.GLOBAL_ANIMATION_RESOURCES, animations, diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_direction.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_direction.gd index 43bfd70..fde2cf0 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_direction.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_direction.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetDirectionCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -55,14 +55,14 @@ func validate(arguments: Array): % [get_command_name(), arguments[0]] ) return false - elif not escoria.object_manager.get_object(arguments[0]).node.is_movable: + if not escoria.object_manager.get_object(arguments[0]).node.is_movable: escoria.logger.error( self, "[%s]: invalid object. Object with global id %s is not movable." % [get_command_name(), arguments[0]] ) return false - elif arguments[1] < 0 or arguments[1] > escoria.object_manager.get_object(arguments[0]).node \ + if arguments[1] < 0 or arguments[1] > escoria.object_manager.get_object(arguments[0]).node \ .get_directions_quantity() - 1: escoria.logger.error( self, diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_global.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_global.gd index 687fe16..f37751a 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_global.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_global.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetGlobalCommand +extends ESCBaseCommand ## The list of illegal strings that cannot be used in global names. const ILLEGAL_STRINGS = ["/"] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_globals.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_globals.gd index 3107057..6122940 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_globals.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_globals.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetGlobalsCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_gui_visible.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_gui_visible.gd index ec83445..d04f10c 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_gui_visible.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_gui_visible.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetGuiVisibleCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_interactive.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_interactive.gd index 007e23a..45a8cfb 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_interactive.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_interactive.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetInteractiveCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_item_custom_data.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_item_custom_data.gd index 2e90ce3..6985c36 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_item_custom_data.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_item_custom_data.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetItemCustomDataCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_speed.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_speed.gd index cd8f2df..8b05814 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_speed.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_speed.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetSpeedCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -49,6 +49,13 @@ func validate(arguments: Array): if not escoria.object_manager.has(arguments[0]): raise_invalid_object_error(self, arguments[0]) return false + + if arguments[1] < 0: + raise_error( + self, + "Invalid speed. Speed cannot be negative (%d)." % arguments[1] + ) + return false return true ## Runs the command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/set_state.gd b/addons/escoria-core/game/core-scripts/esc/commands/set_state.gd index 597751f..4efd6c5 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/set_state.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/set_state.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SetStateCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/show_menu.gd b/addons/escoria-core/game/core-scripts/esc/commands/show_menu.gd index 3ee2935..32d971b 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/show_menu.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/show_menu.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name ShowMenuCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -73,7 +73,7 @@ func run(command_params: Array) -> int: # Transition out from current scene var transition_id = escoria.main.scene_transition.transition( "", - ESCTransitionPlayer.TRANSITION_MODE.OUT + ESCTransitionPlayer.TransitionMode.OUT ) if transition_id != ESCTransitionPlayer.TRANSITION_ID_INSTANT: diff --git a/addons/escoria-core/game/core-scripts/esc/commands/slide.gd b/addons/escoria-core/game/core-scripts/esc/commands/slide.gd index d168dfe..b3b56ab 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/slide.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/slide.gd @@ -14,8 +14,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SlideCommand +extends ESCBaseCommand ## A hash of tweens currently active for animated items diff --git a/addons/escoria-core/game/core-scripts/esc/commands/slide_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/slide_block.gd index 59fa186..397b5d3 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/slide_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/slide_block.gd @@ -13,8 +13,8 @@ ## ## @ASHES ## @COMMAND -extends SlideCommand class_name SlideBlockCommand +extends SlideCommand ## Runs the command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/spawn.gd b/addons/escoria-core/game/core-scripts/esc/commands/spawn.gd index b0f01ba..b699816 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/spawn.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/spawn.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name SpawnCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/stop.gd b/addons/escoria-core/game/core-scripts/esc/commands/stop.gd index b53183f..578bb3e 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/stop.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/stop.gd @@ -8,8 +8,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name StopCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -40,7 +40,7 @@ func configure() -> ESCCommandArgumentDescriptor: ## #### Returns[br] ## [br] ## Returns the execution result code. (`int`) -func run(command_params: Array) -> int: +func run(_command_params: Array) -> int: return ESCExecution.RC_CANCEL diff --git a/addons/escoria-core/game/core-scripts/esc/commands/stop_snd.gd b/addons/escoria-core/game/core-scripts/esc/commands/stop_snd.gd index ae0fc62..5218d3a 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/stop_snd.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/stop_snd.gd @@ -10,15 +10,27 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name StopSndCommand +extends ESCBaseCommand +## The previous sound state, saved for interrupting +var previous_snd_state: String ## The specified sound player var _snd_player: String -## The previous sound state, saved for interrupting -var previous_snd_state: String + +## Constructor of this command.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _init() -> void: + run_in_ready_allowed_during_savegame_loading = false ## The descriptor of the arguments of this command.[br] @@ -93,20 +105,20 @@ func run(command_params: Array) -> int: ## [br] ## Returns nothing. func interrupt(): - var _sound_players = [] + var sound_players = [] if previous_snd_state.is_empty(): previous_snd_state = "off" if _snd_player.is_empty(): - _sound_players = [ + sound_players = [ ESCObjectManager.MUSIC, ESCObjectManager.SOUND, ESCObjectManager.SPEECH ] else: - _sound_players = [_snd_player] + sound_players = [_snd_player] - for snd_player in _sound_players: + for snd_player in sound_players: if escoria.object_manager.get_object(snd_player).node: escoria.object_manager.get_object(snd_player).node.set_state( previous_snd_state diff --git a/addons/escoria-core/game/core-scripts/esc/commands/teleport.gd b/addons/escoria-core/game/core-scripts/esc/commands/teleport.gd index ae41393..f0d7895 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/teleport.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/teleport.gd @@ -11,8 +11,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name TeleportCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/teleport_pos.gd b/addons/escoria-core/game/core-scripts/esc/commands/teleport_pos.gd index bf7a4fe..c561dfc 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/teleport_pos.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/teleport_pos.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name TeleportPosCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/transition.gd b/addons/escoria-core/game/core-scripts/esc/commands/transition.gd index ff2c562..192fcdc 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/transition.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/transition.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name TransitionCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] @@ -78,8 +78,8 @@ func validate(arguments: Array): func run(command_params: Array) -> int: var transition_id = escoria.main.scene_transition.transition( command_params[0], - ESCTransitionPlayer.TRANSITION_MODE.OUT if command_params[1] == "out" \ - else ESCTransitionPlayer.TRANSITION_MODE.IN, + ESCTransitionPlayer.TransitionMode.OUT if command_params[1] == "out" \ + else ESCTransitionPlayer.TransitionMode.IN, command_params[2] ) diff --git a/addons/escoria-core/game/core-scripts/esc/commands/turn_to.gd b/addons/escoria-core/game/core-scripts/esc/commands/turn_to.gd index 6cf6fc4..efb5e7a 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/turn_to.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/turn_to.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name TurnToCommand +extends ESCBaseCommand ## The descriptor of the arguments of this command.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/commands/wait.gd b/addons/escoria-core/game/core-scripts/esc/commands/wait.gd index 1775452..1032a55 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/wait.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/wait.gd @@ -10,8 +10,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name WaitCommand +extends ESCBaseCommand ## Timer to wait for var timer: Timer diff --git a/addons/escoria-core/game/core-scripts/esc/commands/walk.gd b/addons/escoria-core/game/core-scripts/esc/commands/walk.gd index 735e199..b38d816 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/walk.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/walk.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name WalkCommand +extends ESCBaseCommand ## Walking object diff --git a/addons/escoria-core/game/core-scripts/esc/commands/walk_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/walk_block.gd index d144a6d..7218b87 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/walk_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/walk_block.gd @@ -12,8 +12,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name WalkBlockCommand +extends ESCBaseCommand ## Walking object diff --git a/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos.gd b/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos.gd index fcbe673..f8303dc 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name WalkToPosCommand +extends ESCBaseCommand ## Walking object @@ -56,7 +56,7 @@ func validate(arguments: Array): if not escoria.object_manager.has(arguments[0]): raise_error( self, - "Invalid first object. The object to make walk with global id '%s' was not found." % arguments[0] + "Invalid first object. The object to make walk with global id '%s' was not found." % arguments[0] ) return false diff --git a/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos_block.gd b/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos_block.gd index 6800522..7ff1a15 100644 --- a/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos_block.gd +++ b/addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos_block.gd @@ -13,8 +13,8 @@ ## [br] ## @ASHES ## @COMMAND -extends ESCBaseCommand class_name WalkToPosBlockCommand +extends ESCBaseCommand ## Walking object diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd index df14c0b..53d8927 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd @@ -1,20 +1,21 @@ -## Used by the intpreter to track the depth of a `break` statement invoked in a dialog. -extends Object +## Used by the intpreter to track dialog `break` propagation state. class_name ESCBreakCounter +extends Object var _levels_left: int = 0: set = set_levels_left, get = get_levels_left +var _resume_parent_dialog: bool = false -## Sets the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Sets the remaining number of dialog levels to unwind before control should return to a parent dialog, where `levels == 0` means the unwind has reached the parent dialog that should resume.[br] ## [br] ## #### Parameters[br] ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |levels|`int`|Number of dialog nesting levels remaining (0 represents the root).|yes|[br] +## |levels|`int`|Number of dialog nesting levels still to exit before resuming a parent dialog.|yes|[br] ## [br] ## #### Returns[br] ## [br] @@ -23,7 +24,7 @@ func set_levels_left(levels: int) -> void: _levels_left = levels -## Gets the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Gets the remaining number of dialog levels to unwind before control should return to a parent dialog, where `levels == 0` means the unwind has reached the parent dialog that should resume.[br] ## [br] ## #### Parameters[br] ## [br] @@ -31,12 +32,68 @@ func set_levels_left(levels: int) -> void: ## [br] ## #### Returns[br] ## [br] -## Returns a `int` value. (`int`) +## Returns an `int` value. func get_levels_left() -> int: return _levels_left -## Decrements by one the depth from the root of the dialog tree, with `levels == 0` at the root.[br] +## Returns whether more dialog levels still need to be exited before reaching the parent dialog that should resume.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns `true` if additional dialog levels must still be unwound, otherwise `false`. (`bool`) +func has_levels_left() -> bool: + return _levels_left > 0 + + +## Advances the dialog-break state upward by one dialog level.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func advance_up_one_level() -> void: + if _levels_left > 0: + _levels_left -= 1 + + if _levels_left == 0: + _resume_parent_dialog = true + + +## Marks the dialog-break state so the receiving dialog frame resumes its own option loop instead of concluding.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func mark_resume_parent_dialog() -> void: + _resume_parent_dialog = true + + +## Returns whether the receiving dialog frame should resume its option loop.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns `true` if the parent dialog should resume, otherwise `false`. (`bool`) +func should_resume_parent_dialog() -> bool: + return _resume_parent_dialog + + +## Clears the resume-parent-dialog state after it has been consumed by a dialog frame.[br] ## [br] ## #### Parameters[br] ## [br] @@ -45,5 +102,5 @@ func get_levels_left() -> int: ## #### Returns[br] ## [br] ## Returns nothing. -func dec_levels_left() -> void: - _levels_left = _levels_left - 1 +func consume_parent_resume() -> void: + _resume_parent_dialog = false diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd index d63c52b..f89bed9 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd @@ -2,8 +2,8 @@ ## ## Scoping is implemented using this class, including traditional scoping rules. ## Scope binding and resolution are handled in this class. -extends Object class_name ESCEnvironment +extends Object var _enclosing: # ESCEnvironment diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd index 0e8bda8..ee94810 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd @@ -3,8 +3,8 @@ ## All inheriting classes must implement an `accept` method to invoke the visitor. ## For more information on the "Visitor" pattern, see: ## (Visitor Pattern)[https://en.wikipedia.org/wiki/Visitor_pattern] -extends RefCounted class_name ESCGrammarExpr +extends RefCounted ## Method that invokes another method in the visitor against this, the implementing class.[br] @@ -18,5 +18,5 @@ class_name ESCGrammarExpr ## #### Returns[br] ## [br] ## Returns nothing. -func accept(visitor): +func accept(_visitor): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd index 1793f0e..126e8cc 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd @@ -3,8 +3,8 @@ ## All inheriting classes must implement an `accept` method to invoke the visitor. ## For more information on the "Visitor" pattern, see: ## (Visitor Pattern)[https://en.wikipedia.org/wiki/Visitor_pattern] -extends RefCounted class_name ESCGrammarStmt +extends RefCounted ## Method that invokes another method in the visitor against this, the implementing class.[br] @@ -18,5 +18,5 @@ class_name ESCGrammarStmt ## #### Returns[br] ## [br] ## Returns nothing. -func accept(visitor): +func accept(_visitor): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd index 2e9c575..07c197c 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd @@ -154,6 +154,27 @@ class Event extends ESCGrammarStmt: ## [br] signal interrupted + ## Valid event flags[br] + ##[br] + ## * TK: stands for "telekinetic". It means the player won't walk over to + ## the item to say the line.[br] + ## * 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.[br] + ## * NO_UI: stands for "No User Inteface". It hides the UI for the duration of + ##Â the event. Useful when you want something to look like a cut scene but not + ## disable input for skipping dialog.[br] + ## * NO_SAVE: disables saving. Use this in cut scenes and anywhere a + ## badly-timed autosave would leave your game in a messed-up state. + enum { + FLAG_TK = 1, + FLAG_NO_TT = 2, + FLAG_NO_UI = 4, + FLAG_NO_SAVE = 8 + } + + var source: String = "" # TODO: Make proper use of this + var _name: ESCToken: get = get_name var _target: ESCGrammarExprs.Literal: @@ -175,28 +196,6 @@ class Event extends ESCGrammarStmt: var _is_interrupted: bool = false: get = is_interrupted - var source: String = "" # TODO: Make proper use of this - - - ## Valid event flags[br] - ##[br] - ## * TK: stands for "telekinetic". It means the player won't walk over to - ## the item to say the line.[br] - ## * 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.[br] - ## * NO_UI: stands for "No User Inteface". It hides the UI for the duration of - ##Â the event. Useful when you want something to look like a cut scene but not - ## disable input for skipping dialog.[br] - ## * NO_SAVE: disables saving. Use this in cut scenes and anywhere a - ## badly-timed autosave would leave your game in a messed-up state. - enum { - FLAG_TK = 1, - FLAG_NO_TT = 2, - FLAG_NO_UI = 4, - FLAG_NO_SAVE = 8 - } - ## Initialization method. Must be called after instantiation.[br] ## [br] @@ -784,6 +783,8 @@ class Stop extends ESCGrammarStmt: ## say("worker", "Because I work here.")[br] ##``` class DialogOption extends ESCGrammarStmt: + var _translation_key: String: + get = get_translation_key var _option: ESCGrammarExpr: get = get_option var _condition: ESCGrammarExpr: @@ -798,6 +799,7 @@ class DialogOption extends ESCGrammarStmt: ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] + ## |translation_key|`String`|an optional translation key for the option text|yes|[br] ## |option|`ESCGrammarExpr`|the expression whose results will be displayed on screen as an option to be selected alongside any other options at the current level|yes|[br] ## |condition|`ESCGrammarExpr`|an optional condition to be evaluated that is used to determine whether the option should be displayed at all|yes|[br] ## |body|`ESCGrammarStmt`|the block of statements to be executed should this dialog option be selected|yes|[br] @@ -805,12 +807,26 @@ class DialogOption extends ESCGrammarStmt: ## #### Returns[br] ## [br] ## Returns nothing. - func init(option: ESCGrammarExpr, condition: ESCGrammarExpr, body: ESCGrammarStmt): + func init(translation_key: String, option: ESCGrammarExpr, condition: ESCGrammarExpr, body: ESCGrammarStmt): + _translation_key = translation_key _option = option _condition = condition _body = body + ## The translation key for this option, if one exists.[br] + ## [br] + ## #### Parameters[br] + ## [br] + ## None. + ## [br] + ## #### Returns[br] + ## [br] + ## Returns the translation key for this option, if one exists. (`String`) + func get_translation_key() -> String: + return _translation_key + + ## The option expression.[br] ## [br] ## #### Parameters[br] diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd index 0944486..2097890 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd @@ -1,6 +1,6 @@ ## The actual interpreter that processes a parsed ASHES script. -extends RefCounted class_name ESCInterpreter +extends RefCounted ## Used to represent the current player character in scripts. @@ -26,6 +26,7 @@ var _locals: Dictionary = {} # Still, this should be tested extensively, and, if at all possible, with multiple # concurrent events running. var _current_event: ESCGrammarStmts.Event +var _dialog_depth: int = 0 # While most of the time we only run a single event at a time, it is possible for # multiple events to @@ -34,10 +35,12 @@ var _event_stack: Array = [] var _builtin_functions: Array = [ "print" ] +var _channel_name: String = "" -func _init(callables: Array, globals: Dictionary): +func _init(callables: Array, globals: Dictionary, channel_name: String = ""): _globals = ESCEnvironment.new() + _channel_name = channel_name for callable in callables: _globals.define(callable.get_command_name(), callable) @@ -77,6 +80,19 @@ func get_global_values() -> Dictionary: return _globals.get_values() +## Gets the event-manager channel this interpreter is executing on.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the channel name associated with this interpreter run. (`String`) +func get_channel_name() -> String: + return _channel_name + + ## Resets the interpreter, specifically any locally-scoped variables.[br] ## [br] ## #### Parameters[br] @@ -253,13 +269,15 @@ func visit_call_expr(expr: ESCGrammarExprs.Call): self, "Can only call valid commands." ) - else: - return _handle_builtin_function(callee, args) + return ESCExecution.RC_ERROR + + return _handle_builtin_function(callee, args) var command = ESCCommand.new() command.parameters = args command.name = callee.get_command_name() command.parser_token = expr.get_paren_token() + command.channel_name = _channel_name var rc = ESCExecution.RC_OK @@ -282,11 +300,12 @@ func _handle_builtin_function(fn_name: String, args: Array) -> int: match fn_name: 'print': - if args.size() > 1: + if args.size() != 1: escoria.logger.error( self, - "'print' only takes one argument" + "'print' expects exactly one argument" ) + return rc _print(args) rc = ESCExecution.RC_OK @@ -315,13 +334,13 @@ func _print(value): func visit_if_stmt(stmt: ESCGrammarStmts.If): if _is_truthy(await _evaluate(stmt.get_condition())): return await _execute(stmt.get_then_branch()) - else: - for branch in stmt.get_elif_branches(): - if _is_truthy(await _evaluate(branch.get_condition())): - return await _execute(branch) - if stmt.get_else_branch(): - return await _execute(stmt.get_else_branch()) + for branch in stmt.get_elif_branches(): + if _is_truthy(await _evaluate(branch.get_condition())): + return await _execute(branch) + + if stmt.get_else_branch(): + return await _execute(stmt.get_else_branch()) return null @@ -344,6 +363,9 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): if ret is ESCGrammarStmts.Break: break + if _is_terminal_control_flow(ret): + return ret + return null @@ -358,10 +380,23 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): +func visit_pass_stmt(_stmt: ESCGrammarStmts.Pass): pass +func _is_terminal_control_flow(ret) -> bool: + if ret is ESCGrammarStmts.Stop \ + or ret is ESCGrammarStmts.Done \ + or ret is ESCBreakCounter: + return true + + if typeof(ret) == TYPE_INT: + return ret == ESCExecution.RC_ERROR \ + or ret == ESCExecution.RC_INTERRUPTED + + return false + + ## Executes code relevant to interpreting a `stop` statement. Relevant only to dialogs.[br] ## [br] ## #### Parameters[br] @@ -438,14 +473,76 @@ func visit_global_stmt(stmt: ESCGrammarStmts.Global): func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): var dialog: ESCDialog = ESCDialog.new() var rc = ESCExecution.RC_OK + var dialog_args := stmt.get_args() + var dialog_options := stmt.get_options() + + _dialog_depth += 1 + dialog.authored_options_count = dialog_options.size() + + if dialog_args.size() > 0: + var avatar = await _evaluate(dialog_args[0]) + + if avatar != null and not avatar is String and not avatar is StringName: + escoria.logger.error( + self, + "Dialog avatar must evaluate to a String." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.avatar = str(avatar) + + if dialog_args.size() > 1: + var timeout = await _evaluate(dialog_args[1]) + + if not timeout is int and not timeout is float: + escoria.logger.error( + self, + "Dialog timeout must evaluate to a number." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + if timeout < 0: + escoria.logger.error( + self, + "Dialog timeout must be greater than or equal to 0." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.timeout = int(timeout) + + if dialog_args.size() > 2: + var timeout_option = await _evaluate(dialog_args[2]) + + if not timeout_option is int and not timeout_option is float: + escoria.logger.error( + self, + "Dialog timeout option must evaluate to a number." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + if timeout_option < 0: + escoria.logger.error( + self, + "Dialog timeout option must be greater than or equal to 0." + ) + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + + dialog.timeout_option = int(timeout_option) while true: dialog.options = [] - for dialog_option in stmt.get_options(): + for index in range(dialog_options.size()): + var dialog_option = dialog_options[index] var option: ESCDialogOption = ESCDialogOption.new() - # TODO: Translation keys option.source_option = dialog_option + option.source_option_index = index + 1 + option.translation_key = dialog_option.get_translation_key() option.option = await _evaluate(dialog_option.get_option()) if dialog_option.get_condition(): @@ -453,12 +550,23 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): else: option.set_is_valid(true) - dialog.options.append(option) + # Dialog execution should stop once no valid options remain. Keep only + # currently valid options in the presented list so an exhausted dialog + # concludes instead of re-running forever with unselectable entries. + if option.is_valid(): + dialog.options.append(option) if dialog.options.size() == 0: break - if dialog.is_valid() and not _current_event.is_interrupted(): + # If the event was interrupted while a nested command or dialog body was + # active, stop the dialog frame immediately instead of looping and + # presenting options again. + if _current_event and _current_event.is_interrupted(): + _dialog_depth -= 1 + return ESCExecution.RC_INTERRUPTED + + if dialog.is_valid(): var chosen_option = await dialog.run() if chosen_option: @@ -468,22 +576,73 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): "Chosen dialog option (%s) was completed." % chosen_option ) - if execute_ret is ESCGrammarStmts.Break: - var break_tracker: ESCBreakCounter = ESCBreakCounter.new() + # Dialog frames must propagate terminal runtime errors the same + # way normal blocks do. Otherwise an invalid command or similar + # failure inside an option body is treated like normal completion + # and the dialog loop re-displays the same options indefinitely. + if typeof(execute_ret) == TYPE_INT and execute_ret == ESCExecution.RC_ERROR: + _dialog_depth -= 1 + return ESCExecution.RC_ERROR + # An interruption can happen while the chosen option body is still + # running, so check again after it returns before processing normal + # dialog control flow like `break`, `done`, or `stop`. + if _current_event and _current_event.is_interrupted(): + _dialog_depth -= 1 + return ESCExecution.RC_INTERRUPTED + + if execute_ret is ESCGrammarStmts.Break: + var levels_left := 0 if execute_ret.get_levels(): - break_tracker.set_levels_left(await _evaluate(execute_ret.get_levels()) - 1) + levels_left = await _evaluate(execute_ret.get_levels()) - 1 + + # A top-level dialog consumes `break` by concluding the current dialog. + if _dialog_depth == 1: + break + + # Nested dialogs propagate explicit break state upward so parent + # dialog frames can decide whether to keep unwinding or resume. + var break_tracker: ESCBreakCounter = ESCBreakCounter.new() + if levels_left > 0: + break_tracker.set_levels_left(levels_left) else: - break_tracker.set_levels_left(0) + break_tracker.mark_resume_parent_dialog() + _dialog_depth -= 1 return break_tracker - elif execute_ret is ESCGrammarStmts.Done: + if execute_ret is ESCGrammarStmts.Done: + _dialog_depth -= 1 + + if _dialog_depth == 0: + return rc + return execute_ret - elif execute_ret is ESCBreakCounter: - if execute_ret.get_levels_left() > 0: - execute_ret.dec_levels_left() + + if execute_ret is ESCGrammarStmts.Stop: + # `stop` aborts the entire event, so dialog frames must pass it through. + _dialog_depth -= 1 + return execute_ret + + if execute_ret is ESCBreakCounter: + if execute_ret.has_levels_left(): + # Once the unwind reaches the top-most dialog, the remaining + # levels are consumed by concluding that dialog entirely. + if _dialog_depth == 1: + break + + execute_ret.advance_up_one_level() + _dialog_depth -= 1 return execute_ret + if execute_ret.should_resume_parent_dialog(): + # The requested dialog levels have been exited, so this frame + # resumes presenting its own options instead of concluding. + execute_ret.consume_parent_resume() + continue + + break + + _dialog_depth -= 1 return rc @@ -498,7 +657,7 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_dialog_option_stmt(stmt: ESCGrammarStmts.DialogOption): +func visit_dialog_option_stmt(_stmt: ESCGrammarStmts.DialogOption): pass @@ -623,18 +782,28 @@ func visit_binary_expr(expr: ESCGrammarExprs.Binary): return not _is_equal(left_part, right_part) ESCTokenType.TokenType.GREATER: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part > right_part ESCTokenType.TokenType.GREATER_EQUAL: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part >= right_part ESCTokenType.TokenType.LESS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part < right_part ESCTokenType.TokenType.LESS_EQUAL: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part <= right_part ESCTokenType.TokenType.MINUS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part - right_part ESCTokenType.TokenType.PLUS: var check = _check_are_numbers(left_part, expr.get_operator(), right_part, false) @@ -653,9 +822,13 @@ func visit_binary_expr(expr: ESCGrammarExprs.Binary): ) ESCTokenType.TokenType.SLASH: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part / right_part ESCTokenType.TokenType.STAR: var check = _check_are_numbers(left_part, expr.get_operator(), right_part) + if not check: + return null return left_part * right_part return null @@ -680,6 +853,8 @@ func visit_unary_expr(expr: ESCGrammarExprs.Unary): return not _is_truthy(right_part) ESCTokenType.TokenType.MINUS: var check = _check_is_number(right_part, expr.get_operator()) + if not check: + return null return -right_part return null @@ -793,8 +968,8 @@ func look_up_variable(name: ESCToken, expr: ESCGrammarExpr): if distance == -1: return _globals.get_value(name) - else: - return _environment.get_at(distance, name.get_lexeme()) + + return _environment.get_at(distance, name.get_lexeme()) # Private methods @@ -860,10 +1035,18 @@ func _execute_block(statements: Array, env: ESCEnvironment): for stmt in statements: ret = await _execute(stmt) - if ret is ESCGrammarStmts.Break \ - or ret is ESCGrammarStmts.Done \ - or ret is ESCGrammarStmts.Stop: + # If the running event was interrupted while this statement was active, + # stop the block immediately so later statements do not continue running. + if _current_event and _current_event.is_interrupted(): + _environment = previous_env + return ESCExecution.RC_INTERRUPTED + if ret is ESCGrammarStmts.Break: + _environment = previous_env + return ret + + if _is_terminal_control_flow(ret): + _environment = previous_env return ret # TODO: Proper error handling per statement? @@ -925,12 +1108,12 @@ func _check_at_least_one_string(value_1, value_2): return typeof(value_1) == TYPE_STRING || typeof(value_2) == TYPE_STRING -func _on_global_changed(key: String, old_value, new_value) -> void: +func _on_global_changed(key: String, _old_value, new_value) -> void: # Shoehorn this in as an adapter var token: ESCToken = ESCToken.new() token.init(ESCTokenType.TokenType.IDENTIFIER, key, null, "", -1, "") if _globals.get_values().has(key): _globals.assign(token, new_value) - elif escoria.save_manager.is_loading_game: + else: _globals.define(key, new_value) diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd index 4205f92..24f6c66 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd @@ -1,6 +1,10 @@ ## Factory class for producing an ASHES interpreter. -extends RefCounted class_name ESCInterpreterFactory +extends RefCounted + +const ESC_INTERPRETER_SCRIPT := preload( + "res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd" +) static var _interpreter: ESCInterpreter = null @@ -15,16 +19,64 @@ static var _interpreter: ESCInterpreter = null ## #### Returns[br] ## [br] ## Returns a `ESCInterpreter` value. (`ESCInterpreter`) -static func create_interpreter() -> ESCInterpreter: - if not _interpreter: - _interpreter = load("res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd").new(ESCCompiler.load_commands(), ESCCompiler.load_globals()) - return _interpreter +static func create_interpreter(channel_name: String = "") -> ESCInterpreter: + return create_utility_interpreter(channel_name) + - _interpreter = load("res://addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd").new([], _interpreter.get_global_values()) +## Produces an interpreter for non-runtime utility work such as resolving or +## evaluating statements outside the event manager.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |channel_name|`String`|Optional channel name to assign to the created interpreter.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a cached-or-reseeded `ESCInterpreter` instance for utility usage. (`ESCInterpreter`) +static func create_utility_interpreter(channel_name: String = "") -> ESCInterpreter: + if not _interpreter: + _interpreter = ESC_INTERPRETER_SCRIPT.new( + ESCCompiler.load_commands(), + ESCCompiler.load_globals(), + channel_name + ) + else: + _interpreter = ESC_INTERPRETER_SCRIPT.new( + [], + _interpreter.get_global_values(), + channel_name + ) return _interpreter +## Produces a fresh interpreter intended for event-manager runtime execution.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |channel_name|`String`|Optional channel name to assign to the created interpreter.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a newly-created `ESCInterpreter` instance for runtime usage. (`ESCInterpreter`) +static func create_runtime_interpreter(channel_name: String = "") -> ESCInterpreter: + var runtime_globals := ESCCompiler.load_globals() + + if not Engine.is_editor_hint() and escoria and escoria.globals_manager: + runtime_globals.merge(escoria.globals_manager.get_globals(), true) + + var interpreter := ESC_INTERPRETER_SCRIPT.new( + ESCCompiler.load_commands(), + runtime_globals, + channel_name + ) + return interpreter + + static func reset_interpreter() -> void: if is_instance_valid(_interpreter): _interpreter.cleanup() diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd index 5fd01a3..1bb5b19 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd @@ -1,3 +1,3 @@ ## Class to represent an error that occured during parsing. -extends Object class_name ESCParseError +extends Object diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd index b88adcb..6229e34 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd @@ -4,8 +4,8 @@ ## Note that the vast majority of this class consists of (effectively) private methods ## in order to facilitate encapsulation, but if you're interested in how the parser ## is implemented, [check out the class in GitHub](https://github.com/godot-escoria/escoria-demo-game/blob/main/addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd) -extends RefCounted class_name ESCParser +extends RefCounted var _tokens: Array @@ -52,48 +52,56 @@ func parse() -> Array: var statements: Array = [] while not _at_end(): - statements.append(_declaration()) + var declaration = _declaration() + if declaration != null: + statements.append(declaration) return statements func _declaration() -> ESCGrammarStmt: - var retStmt + while _match(ESCTokenType.TokenType.NEWLINE) or _match(ESCTokenType.TokenType.DEDENT): + pass + + if _at_end(): + return null + + var ret_stmt if _match(ESCTokenType.TokenType.COLON): - retStmt = _event_declaration() + ret_stmt = _event_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt + + return ret_stmt if _match(ESCTokenType.TokenType.VAR): - retStmt = _var_declaration() + ret_stmt = _var_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt + + return ret_stmt if _match(ESCTokenType.TokenType.GLOBAL): - retStmt = _global_declaration() + ret_stmt = _global_declaration() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - else: - return retStmt - retStmt = _statement() + return ret_stmt + + ret_stmt = _statement() - if retStmt is ESCParseError: + if ret_stmt is ESCParseError: _synchronize() return null - return retStmt + return ret_stmt func _event_declaration(): @@ -157,8 +165,8 @@ func _event_declaration(): ret.init(name, target, flags, body, _associated_object_global_id) return ret - else: - return _error(_peek(), "Expected block after event declaration for '%s'. Code blocks require tab(s) at the start of a line." % name.get_lexeme()) + + return _error(_peek(), "Expected block after event declaration for '%s'. Code blocks require tab(s) at the start of a line." % name.get_lexeme()) func _literal_from_identifier(expr: ESCGrammarExprs.Variable) -> ESCGrammarExprs.Literal: @@ -314,21 +322,25 @@ func _while_statement(): var condition = _expression() if condition is ESCParseError: + _loop_level -= 1 return condition var colon_token = _consume(ESCTokenType.TokenType.COLON, "Expect ':' after condition.") if colon_token is ESCParseError: + _loop_level -= 1 return colon_token var consume = _consume_new_block_start("start of while loop") if consume: + _loop_level -= 1 return consume var block = _block() if block is ESCParseError: + _loop_level -= 1 return block var body = ESCGrammarStmts.Block.new() @@ -372,6 +384,7 @@ func _dialog_statement(): var arg = _expression() if arg is ESCParseError: + _dialog_level -= 1 return arg args.append(arg) @@ -382,14 +395,32 @@ func _dialog_statement(): var consume = _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after start dialog arguments.") if consume is ESCParseError: + _dialog_level -= 1 return consume + elif not _check(ESCTokenType.TokenType.NEWLINE): + while true: + var arg = _expression() + + if arg is ESCParseError: + _dialog_level -= 1 + return arg + + args.append(arg) + + if _check(ESCTokenType.TokenType.NEWLINE): + break + + if _match(ESCTokenType.TokenType.COMMA): + continue if args.size() > 3: + _dialog_level -= 1 return _error(_peek(), "Start dialog cannot have more than 3 arguments.") var consume = _consume_new_block_start("dialog start") if consume is ESCParseError: + _dialog_level -= 1 return consume var options: Array = [] @@ -398,6 +429,7 @@ func _dialog_statement(): var dialog_option = _dialog_option_statement() if dialog_option is ESCParseError: + _dialog_level -= 1 return dialog_option options.append(dialog_option) @@ -433,6 +465,14 @@ func _dialog_option_statement(): if consume is ESCParseError: return consume + var translation_key := "" + + if _match_in_order([ + ESCTokenType.TokenType.IDENTIFIER, + ESCTokenType.TokenType.COLON + ]): + translation_key = _tokens[_current - 2].get_lexeme() + var expr = _expression() if expr is ESCParseError: @@ -448,8 +488,14 @@ func _dialog_option_statement(): consume = _consume(ESCTokenType.TokenType.RIGHT_SQUARE, "Expect ']' after dialog option condition") + if consume is ESCParseError: + return consume + consume = _consume_new_block_start("dialog option") + if consume is ESCParseError: + return consume + var block_stmts = _block() if block_stmts is ESCParseError: @@ -459,7 +505,7 @@ func _dialog_option_statement(): block.init(block_stmts) var option: ESCGrammarStmts.DialogOption = ESCGrammarStmts.DialogOption.new() - option.init(expr, condition, block) + option.init(translation_key, expr, condition, block) return option @@ -512,7 +558,8 @@ func _block(): if decl is ESCParseError: return decl - statements.append(decl) + if decl != null: + statements.append(decl) _consume(ESCTokenType.TokenType.DEDENT, "Expected DEDENT after block.") return statements @@ -536,7 +583,8 @@ func _assignment(): var ret = ESCGrammarExprs.Assign.new() ret.init(name, value) return ret - elif expr is ESCGrammarExprs.Get: + + if expr is ESCGrammarExprs.Get: var ret = ESCGrammarExprs.Set.new() ret.init(expr.get_object(), expr.get_name(), value) return ret @@ -723,15 +771,15 @@ func _is_checking(): var ret = ESCGrammarExprs.Is.new() ret.init(expr, null, _previous()) return ret - else: - var state_expr = _expression() - if state_expr is ESCParseError: - return state_expr + var state_expr = _expression() - var ret = ESCGrammarExprs.Is.new() - ret.init(expr, state_expr, null) - return ret + if state_expr is ESCParseError: + return state_expr + + var ret = ESCGrammarExprs.Is.new() + ret.init(expr, state_expr, null) + return ret return expr @@ -777,6 +825,9 @@ func _finish_call(callee: ESCGrammarExpr): var paren = _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after arguments.") + if paren is ESCParseError: + return paren + #if paren.get_type() != ESCTokenType.TokenType.NEWLINE: # return _error(ESCTokenType.TokenType.NEWLINE, "Expect NEWLINE after arguments.") @@ -817,7 +868,13 @@ func _primary(): if expr is ESCParseError: return expr - _consume(ESCTokenType.TokenType.RIGHT_PAREN, "Expect ')' after expression.") + var paren = _consume( + ESCTokenType.TokenType.RIGHT_PAREN, + "Expect ')' after expression." + ) + + if paren is ESCParseError: + return paren var ret = ESCGrammarExprs.Grouping.new() ret.init(expr) @@ -826,30 +883,52 @@ func _primary(): return _error(_peek(), "Expect expression.") -func _var_declaration() -> ESCGrammarStmt: +func _var_declaration(): var name = _consume(ESCTokenType.TokenType.IDENTIFIER, "Expect variable name.") var initializer: ESCGrammarExpr = null if _match(ESCTokenType.TokenType.EQUAL): - initializer = _expression() + var initializer_expr = _expression() + + if initializer_expr is ESCParseError: + return initializer_expr + + initializer = initializer_expr + + var newline = _consume( + ESCTokenType.TokenType.NEWLINE, + "Expect newline after variable declaration." + ) - _consume(ESCTokenType.TokenType.NEWLINE, "Expect newline after variable declaration.") + if newline is ESCParseError: + return newline var ret = ESCGrammarStmts.Var.new() ret.init(name, initializer) return ret -func _global_declaration() -> ESCGrammarStmt: +func _global_declaration(): var name = _consume(ESCTokenType.TokenType.IDENTIFIER, "Expect global variable name.") var initializer: ESCGrammarExpr = null if _match(ESCTokenType.TokenType.EQUAL): - initializer = _expression() + var initializer_expr = _expression() - _consume(ESCTokenType.TokenType.NEWLINE, "Expect newline after global variable declaration.") + if initializer_expr is ESCParseError: + return initializer_expr + + initializer = initializer_expr + + var newline = _consume( + ESCTokenType.TokenType.NEWLINE, + "Expect newline after global variable declaration." + ) + + if newline is ESCParseError: + return newline var ret = ESCGrammarStmts.Global.new() ret.init(name, initializer) @@ -864,11 +943,11 @@ func _peek() -> ESCToken: return _tokens[_current] -func _match(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _match(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] - for type in tokenTypes: + for type in token_types: if _check(type): _advance() return true @@ -876,12 +955,15 @@ func _match(tokenTypes) -> bool: return false -func _match_in_order(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _match_in_order(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] - for type in tokenTypes: + var start := _current + + for type in token_types: if not _check(type): + _current = start return false _advance() @@ -889,21 +971,21 @@ func _match_in_order(tokenTypes) -> bool: return true -func _consume(tokenType, message: String): - if _check(tokenType): +func _consume(token_type, message: String): + if _check(token_type): return _advance() return _error(_peek(), message) -func _check(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _check(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] if _at_end(): return false - for type in tokenTypes: + for type in token_types: if _peek().get_type() == type: return true @@ -911,9 +993,9 @@ func _check(tokenTypes) -> bool: # This turns the parser into an LL(2). -func _check_next(tokenTypes) -> bool: - if not tokenTypes is Array: - tokenTypes = [tokenTypes] +func _check_next(token_types) -> bool: + if not token_types is Array: + token_types = [token_types] if _at_end(): return false @@ -924,7 +1006,7 @@ func _check_next(tokenTypes) -> bool: _current -= 1 return false - for type in tokenTypes: + for type in token_types: if _peek().get_type() == type: _current -= 1 return true @@ -959,16 +1041,27 @@ func _error(token: ESCToken, message: String) -> ESCParseError: func _synchronize() -> void: + if _check(ESCTokenType.TokenType.DEDENT): + return + _advance() while not _at_end(): - if _previous().get_type() == ESCTokenType.TokenType.NEWLINE: + if _previous().get_type() == ESCTokenType.TokenType.NEWLINE \ + or _previous().get_type() == ESCTokenType.TokenType.DEDENT: return match _peek().get_type(): + ESCTokenType.TokenType.COLON,\ + ESCTokenType.TokenType.GLOBAL,\ ESCTokenType.TokenType.VAR,\ ESCTokenType.TokenType.IF,\ ESCTokenType.TokenType.WHILE,\ + ESCTokenType.TokenType.PASS,\ + ESCTokenType.TokenType.STOP,\ + ESCTokenType.TokenType.QUESTION_BANG,\ + ESCTokenType.TokenType.BREAK,\ + ESCTokenType.TokenType.DONE,\ ESCTokenType.TokenType.RETURN: return diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd index 2b32b35..d356b85 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd @@ -1,8 +1,8 @@ ## Resolves script variables to ensure that scoping rules are obeyed. ## ## The class will throw errors if scoping rules are broken or otherwise not followed. -extends RefCounted class_name ESCResolver +extends RefCounted var _interpreter @@ -136,7 +136,7 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): +func visit_pass_stmt(_stmt: ESCGrammarStmts.Pass): pass @@ -151,7 +151,7 @@ func visit_pass_stmt(stmt: ESCGrammarStmts.Pass): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_stop_stmt(stmt: ESCGrammarStmts.Stop): +func visit_stop_stmt(_stmt: ESCGrammarStmts.Stop): pass @@ -186,7 +186,7 @@ func visit_var_stmt(stmt: ESCGrammarStmts.Var): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_global_stmt(stmt: ESCGrammarStmts.Global): +func visit_global_stmt(_stmt: ESCGrammarStmts.Global): pass @@ -237,7 +237,7 @@ func visit_break_stmt(stmt: ESCGrammarStmts.Break): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_done_stmt(stmt: ESCGrammarStmts.Done): +func visit_done_stmt(_stmt: ESCGrammarStmts.Done): pass @@ -270,7 +270,7 @@ func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog): ## #### Returns[br] ## [br] ## Returns nothing. -func visit_literal_expr(expr: ESCGrammarExprs.Literal): +func visit_literal_expr(_expr: ESCGrammarExprs.Literal): pass diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd index 08642a9..6591c48 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd @@ -3,8 +3,8 @@ ## This is the first link in the ASHES compiler toolchain, and takes in ASHES ## script as text in order to provide a list of tokens to be handled by the ## ASHES parser. -extends RefCounted class_name ESCScanner +extends RefCounted ## In ASHES, an identifier starting with `$` is interpreted as a lookup in diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd index 46ba408..26dacce 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd @@ -1,7 +1,7 @@ ## Convenience class to help build scripts in code for use with the ASHES compiler ## toolchain. Loosely follows the Builder pattern. -extends RefCounted class_name ESCScriptBuilder +extends RefCounted # Not really a "Builder" pattern per se, but helps with readability. diff --git a/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd b/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd index ec95d98..bd99d73 100644 --- a/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd +++ b/addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd @@ -3,11 +3,11 @@ ## ## This class contains any information required by the parser but also carries ## with it associated information useful for debugging and tracing purposes. -extends RefCounted class_name ESCToken +extends RefCounted -var _tokenType: int: # ESCScanner.TokenType enum +var _token_type: int: # ESCScanner.TokenType enum get = get_type var _lexeme: String: get = get_lexeme @@ -38,8 +38,8 @@ var _filename: String: ## #### Returns[br] ## [br] ## Returns nothing. -func init(tokenType: int, lexeme: String, literal, source: String, line: int, filename: String) -> void: - _tokenType = tokenType +func init(token_type: int, lexeme: String, literal, source: String, line: int, filename: String) -> void: + _token_type = token_type _lexeme = lexeme _literal = literal _source = source @@ -85,7 +85,7 @@ func get_source() -> String: ## [br] ## Returns the type of token this is; will be a value from `ESCTokenType.TokenType`. (`int`) func get_type() -> int: - return _tokenType + return _token_type ## The literal value of the token, if it exists.[br] @@ -141,4 +141,4 @@ func get_filename() -> String: func _to_string() -> String: - return ESCTokenType.get_token_type_name(_tokenType) + " " + str(_lexeme) + " " + (str(_literal) if _literal else "") + return ESCTokenType.get_token_type_name(_token_type) + " " + str(_lexeme) + " " + (str(_literal) if _literal else "") diff --git a/addons/escoria-core/game/core-scripts/esc/esc_action_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_action_manager.gd index 7bf4a37..8e5c596 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_action_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_action_manager.gd @@ -1,11 +1,7 @@ ## Manages actions currently being carried out. ## @MANAGER -extends Resource class_name ESCActionManager - - -## ESCPlayer resource -const ESCPlayer = preload("res://addons/escoria-core/game/core-scripts/esc_player.gd") +extends Resource ## The current action verb was changed.[br] @@ -39,10 +35,10 @@ signal action_input_state_changed ## (I) -> AWAITING_VERB_OR_ITEM -> AWAITING_ITEM -> AWAITING_TARGET_ITEM -> COMPLETED -> (E)[br] ## or[br] ## (I) -> AWAITING_VERB_OR_ITEM -> AWAITING_VERB -> AWAITING_VERB_CONFIRMATION -> COMPLETED -> (E) -enum ACTION_INPUT_STATE { +enum ActionInputState { AWAITING_VERB_OR_ITEM, ## Initial state AWAITING_ITEM, ## After initial state, verb is defined - AWAITING_TARGET_ITEM, ## Item defined requires combine, waiting for target + AWAITING_TARGET_ITEM, ## Item defined requires a target object AWAITING_VERB, ## After initial state, item is defined AWAITING_VERB_CONFIRMATION, ## Item was defined first, next verb, need verb confirmation COMPLETED ## Final state @@ -65,6 +61,9 @@ enum ACTION { # Basic required internal actions +## ESCPlayer resource +const ESCPlayer = preload("res://addons/escoria-core/game/core-scripts/esc_player.gd") + ## Action (event) triggered when a character has reached a destination. const ACTION_ARRIVED = "arrived" @@ -85,7 +84,7 @@ var current_tool: ESCObject var current_target: ESCObject ## Current action input state. -var action_state = ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM: +var action_state = ActionInputState.AWAITING_VERB_OR_ITEM: set = set_action_input_state @@ -103,7 +102,7 @@ var action_state = ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM: ## [br] ## Returns nothing. func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: - if escoria.current_state == escoria.GAME_STATE.DEFAULT: + if escoria.current_state == escoria.GameState.DEFAULT: match action: ACTION.BACKGROUND_CLICK: if can_interrupt: @@ -172,6 +171,7 @@ func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: var trigger_id = params[0] var object_id = params[1] var trigger_in_verb = params[2] + var trigger_object := escoria.object_manager.get_object(trigger_id) escoria.logger.info( self, "trigger_in on trigger %s activated by %s." % [ @@ -179,12 +179,17 @@ func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: object_id ] ) - if trigger_in_verb in escoria.object_manager.get_object(trigger_id).events: - escoria.event_manager.queue_event( - escoria.object_manager.get_object(trigger_id).events[ - trigger_in_verb - ] + + if not _is_object_actionable(trigger_object): + escoria.logger.info( + self, + "Trigger %s is not actionable. Skipping trigger_in." % trigger_id ) + return + + var trigger_in_event = _get_trigger_event(trigger_object, trigger_in_verb, object_id) + if trigger_in_event: + escoria.event_manager.queue_event(trigger_in_event) else: escoria.logger.info( self, @@ -195,6 +200,7 @@ func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: var trigger_id = params[0] var object_id = params[1] var trigger_out_verb = params[2] + var trigger_object := escoria.object_manager.get_object(trigger_id) escoria.logger.info( self, "trigger_out on trigger %s activated by %s." % [ @@ -202,12 +208,21 @@ func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: object_id ] ) - if trigger_out_verb in escoria.object_manager.get_object(trigger_id).events: - escoria.event_manager.queue_event( - escoria.object_manager.get_object(trigger_id).events[ - trigger_out_verb - ] + + if not _is_object_actionable(trigger_object): + escoria.logger.info( + self, + "Trigger %s is not actionable. Skipping trigger_out." % trigger_id ) + return + + var trigger_out_event = _get_trigger_event( + trigger_object, + trigger_out_verb, + object_id + ) + if trigger_out_event: + escoria.event_manager.queue_event(trigger_out_event) else: escoria.logger.info( self, @@ -220,12 +235,19 @@ func do(action: int, params: Array = [], can_interrupt: bool = false) -> void: self, "Action received: '%s' with params %s." % [action, params] ) - elif escoria.current_state == escoria.GAME_STATE.WAIT: + elif escoria.current_state == escoria.GameState.WAIT: pass - elif escoria.current_state == escoria.GAME_STATE.LOADING: + elif escoria.current_state == escoria.GameState.LOADING: pass +func _get_trigger_event(trigger_object: ESCObject, trigger_verb: String, object_id: String): + if trigger_object.has_event_with_target(trigger_verb, object_id): + return trigger_object.get_event_with_target(trigger_verb, object_id) + + return trigger_object.get_event_with_target(trigger_verb) + + ## Sets the current state of action input. ## Parameters[br] ## [br] ## #### Parameters[br] @@ -242,7 +264,7 @@ func set_action_input_state(p_state) -> void: action_input_state_changed.emit() -## Sets the current action verb. ## Parameters[br] +## Sets the current action verb. ## [br] ## #### Parameters[br] ## [br] @@ -259,10 +281,10 @@ func set_current_action(action: String) -> void: current_action = action - if action_state == ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_ITEM) - elif action_state == ACTION_INPUT_STATE.AWAITING_VERB: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_VERB_CONFIRMATION) + if action_state == ActionInputState.AWAITING_VERB_OR_ITEM: + set_action_input_state(ActionInputState.AWAITING_ITEM) + elif action_state == ActionInputState.AWAITING_VERB: + set_action_input_state(ActionInputState.AWAITING_VERB_CONFIRMATION) action_changed.emit() @@ -278,7 +300,7 @@ func set_current_action(action: String) -> void: ## Returns nothing. func clear_current_action() -> void: set_current_action("") - set_action_input_state(ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM) + set_action_input_state(ActionInputState.AWAITING_VERB_OR_ITEM) action_changed.emit() @@ -294,10 +316,10 @@ func clear_current_action() -> void: func clear_current_tool() -> void: current_tool = null current_target = null - if action_state == ACTION_INPUT_STATE.AWAITING_VERB: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM) - elif action_state == ACTION_INPUT_STATE.AWAITING_TARGET_ITEM: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_ITEM) + if action_state == ActionInputState.AWAITING_VERB: + set_action_input_state(ActionInputState.AWAITING_VERB_OR_ITEM) + elif action_state == ActionInputState.AWAITING_TARGET_ITEM: + set_action_input_state(ActionInputState.AWAITING_ITEM) ## Checks if the specified action is valid and returns the associated event; otherwise, we see if there's a "fallback" event and use that if necessary and, if not, we return no event as there's nothing to do. or there's a reason not to run an event.[br] @@ -326,9 +348,9 @@ func _get_event_to_queue( var event_to_return = null - # If we're using an action which item requires to combine + # If the action makes this item wait for a second target object. if target.node is ESCItem \ - and action in target.node.combine_when_selected_action_is_in: + and action in target.node.actions_requiring_target_object: # Check if object is in inventory or is not required to be in inventory to be used if escoria.inventory_manager.inventory_has(target.global_id) or not target.node.use_from_inventory_only: @@ -343,28 +365,19 @@ func _get_event_to_queue( do_combine = false if do_combine: - var action_name = action - if combine_with.global_id: - action_name += " " + combine_with.global_id - - var combine_name = action - if combine_with.global_id: - combine_name += " " + target.global_id - - if _has_event_with_target(target.events, action_name, combine_with.global_id): - event_to_return = target.events[action_name] - - elif _has_event_with_target(combine_with.events, combine_name, target.global_id)\ + if target.has_event_with_target(action, combine_with.global_id): + event_to_return = target.get_event_with_target(action, combine_with.global_id) + elif combine_with.has_event_with_target(action, target.global_id)\ and not combine_with.node.combine_is_one_way: - event_to_return = combine_with.events[combine_name] + event_to_return = combine_with.get_event_with_target(action, target.global_id) else: # Check to see if there isn't a "fallback" action to # run before we declare this a failure. if escoria.action_default_script \ - and escoria.action_default_script.events.has(action): + and escoria.action_default_script.has_event_with_target(action): - event_to_return = escoria.action_default_script.events[action] + event_to_return = escoria.action_default_script.get_event_with_target(action) else: var errors = [ "Attempted to execute action '%s' between item %s and item %s" % [ @@ -414,17 +427,14 @@ func _get_event_to_queue( + "but item must be in inventory." ) else: - if _check_target_has_proper_action(target, action): -# ##SAVEGAME -# if target.events[action].is_completed: -# target.events[action].is_completed = false -# target.events[action].from_statement_id = 0 - event_to_return = target.events[action] + if target.has_event_with_target(action): + # We only deal with the action here as long as it doesn't have a target associated with it. + event_to_return = target.get_event_with_target(action) elif escoria.action_default_script \ - and escoria.action_default_script.events.has(action): + and escoria.action_default_script.has_event_with_target(action): # If there's a "fallback" action to run, return it - event_to_return = escoria.action_default_script.events[action] + event_to_return = escoria.action_default_script.get_event_with_target(action) else: escoria.logger.warn( self, @@ -438,21 +448,23 @@ func _get_event_to_queue( return event_to_return -## Check to make sure `target` contains the specific `action`. If `target` has an entry for `action` that also requires a target itself (e.g. `:use "wrench"`), then we return `false` as combinations are handled elsewhere.[br] +## Check to make sure `target_object` contains the specific `action`. If `target_object` has an entry for `action` that also requires a target itself (e.g. `:use "wrench"`), then we return `false` as targeted item actions are handled elsewhere.[br] +## [br] +## (The "target" in `target_object` is of a different meaning than the optional "target" in an event.) ## [br] ## #### Parameters[br] ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |target|`ESCObject`|`ESCObject` whose events we are to check to see if `action` has a corresponding event.|yes|[br] +## |target_object|`ESCObject`|`ESCObject` whose events we are to check to see if `action` has a corresponding event.|yes|[br] ## |action|`String`|The action to check.|yes|[br] ## [br] ## #### Returns[br] ## [br] ## Returns a `bool` value. (`bool`) -func _check_target_has_proper_action(target: ESCObject, action: String) -> bool: - if target.events.has(action): - if target.events[action].get_target(): +func _check_target_has_proper_action(target_object: ESCObject, action: String) -> bool: + if target_object.has_event_with_target(action): + if target_object.get_event_with_target(action).get_target(): return false return true @@ -460,26 +472,6 @@ func _check_target_has_proper_action(target: ESCObject, action: String) -> bool: return false -## Determines whether the specified events dictionary contains an event with the specified event name and event target, e.g. :give "filled_out_form".[br] -## [br] -## #### Parameters[br] -## [br] -## | Name | Type | Description | Required? |[br] -## |:-----|:-----|:------------|:----------|[br] -## |events_dict|`Dictionary`|dictionary with events to check.|yes|[br] -## |event_name|`String`|the event name to search for.|yes|[br] -## |event_target|`String`|the target for the specified event to check.|yes|[br] -## [br] -## #### Returns[br] -## [br] -## Returns nothing. -func _has_event_with_target(events_dict: Dictionary, event_name: String, event_target: String): - var event = events_dict.get(event_name) - if event == null: - return false - return true - - ## Runs the specified event.[br] ## [br] ## #### Parameters[br] @@ -523,7 +515,6 @@ func perform_walk( destination, is_fast: bool = false ) -> void: - # Walk to Position2D. if destination is Vector2: var walk_context = ESCWalkContext.new( null, @@ -578,29 +569,9 @@ func perform_inputevent_on_object( event: InputEvent, default_action: bool = false ) -> void: - """ - This algorithm: - - validates the requested action - - grabs the corresponding event for the action, if available - - makes the player move to the clicked object location, if needed - (if it is located in the room for example) and wait for reaching. - - when reached, performs an action depending on current defined action - * no current action defined: do nothing else - * current action defined: - * item requires no combination: perform the current action - on the item - * item requires combination: check the status of the combination - A combination requires 3 elements to fulfill: - 1/ a verb action - 2/ a first "tool" (item to use) - 3/ a second "tool" (item to use ON) - Whatever the user inputs to fulfill the combination (this is - determined by gamedev in his game.gd script) - - combination not fulfilled: no not perform until fulfilled - - combination fulfilled: perform the combination. - * else do nothing, except if default_action is requested. - In this case, perform the default_action on the item. - """ + # This validates the requested action, resolves the corresponding event, + # moves the player when needed, and only performs targeted item actions + # after verb + source item + target item have all been selected. escoria.logger.info( self, @@ -612,30 +583,30 @@ func perform_inputevent_on_object( var dont_interact = false #Â We need to have the new action input state BEFORE initiating the player - # move so we determine now if the object clicked will require a combination + # move so we determine now if the object clicked requires a target object # depending on the used action verb. var tool_just_set = _set_tool_and_action(obj, default_action) - var need_combine = _check_item_needs_combine() + var requires_target_object = _check_item_requires_target_object() # If the clicked item is not in the inventory and the current tool was not set, # then this is our first item, make it the tool. if (not escoria.inventory_manager.inventory_has(obj.global_id) and not current_tool) \ - or (current_tool and not need_combine): + or (current_tool and not requires_target_object): current_tool = obj - # Else, if we have a tool and combination required, this is our second item, + # Else, if we have a tool and a target is required, this is our second item, # make it the target. - elif need_combine and not tool_just_set: + elif requires_target_object and not tool_just_set: current_target = obj # Update the action input state - if action_state == ACTION_INPUT_STATE.AWAITING_TARGET_ITEM and current_target: - set_action_input_state(ACTION_INPUT_STATE.COMPLETED) - elif action_state == ACTION_INPUT_STATE.AWAITING_ITEM and \ - not need_combine: - set_action_input_state(ACTION_INPUT_STATE.COMPLETED) - elif action_state == ACTION_INPUT_STATE.AWAITING_ITEM and \ - need_combine and not tool_just_set: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_TARGET_ITEM) + if action_state == ActionInputState.AWAITING_TARGET_ITEM and current_target: + set_action_input_state(ActionInputState.COMPLETED) + elif action_state == ActionInputState.AWAITING_ITEM and \ + not requires_target_object: + set_action_input_state(ActionInputState.COMPLETED) + elif action_state == ActionInputState.AWAITING_ITEM and \ + requires_target_object and not tool_just_set: + set_action_input_state(ActionInputState.AWAITING_TARGET_ITEM) var event_to_queue: ESCGrammarStmts.Event = null @@ -648,19 +619,19 @@ func perform_inputevent_on_object( # If so, and if the object is not in the inventory, we need to run the arrived action. elif current_action in ["", ACTION_WALK] and not escoria.inventory_manager.inventory_has(obj.global_id): event_to_queue = _get_event_to_queue(ACTION_ARRIVED, obj) - # If the current action is set and different from "walk"/unset, we need to check for combinations. + # If the current action is set and different from "walk"/unset, check for targeted actions. elif not current_action in ["", ACTION_WALK]: - # If clicked object needs a combination, and current target is set, then perform the combination. - if need_combine and current_target: + # If the selected tool requires a target and a target is set, run the targeted action. + if requires_target_object and current_target: event_to_queue = _get_event_to_queue(current_action, current_tool, current_target) - # If clicked object needs a combination then we need to wait for the target. - elif need_combine: - set_action_input_state(ACTION_INPUT_STATE.AWAITING_TARGET_ITEM) + # If the selected tool requires a target, wait for the player to choose one. + elif requires_target_object: + set_action_input_state(ActionInputState.AWAITING_TARGET_ITEM) # If object is in inventory make it current tool. if escoria.inventory_manager.inventory_has(obj.global_id): current_tool = obj return - # If clicked object doesn't need a combination, then we simply run the action. + # If clicked object doesn't need a target, then we simply run the action. else: event_to_queue = _get_event_to_queue(current_action, obj) @@ -707,8 +678,8 @@ func perform_inputevent_on_object( "Player could not reach destination coordinates %s. " % str(destination_position) \ + "Any requested action for %s will not fire." % obj.global_id ) - if escoria.event_manager.EVENT_CANT_REACH in obj.events: - escoria.event_manager.queue_event(obj.events[escoria.event_manager.EVENT_CANT_REACH]) + if obj.has_event_with_target(escoria.event_manager.EVENT_CANT_REACH): + escoria.event_manager.queue_event(obj.get_event_with_target(escoria.event_manager.EVENT_CANT_REACH)) else: escoria.logger.info( self, @@ -723,6 +694,8 @@ func perform_inputevent_on_object( func _telekinetic_applies_to(event: ESCGrammarStmts.Event) -> bool: + if event == null: + return false if event.get_flags_with_conditions().has("TK"): var tk_flag_condition = event.get_flags_with_conditions().get("TK") @@ -772,7 +745,7 @@ func _set_tool_and_action(obj: ESCObject, default_action: bool): # Check if current_action and current_tool are already set if current_action and current_tool: if not current_action in escoria.action_manager \ - .current_tool.node.combine_when_selected_action_is_in: + .current_tool.node.actions_requiring_target_object: current_tool = obj tool_just_set = true elif default_action: @@ -780,13 +753,13 @@ func _set_tool_and_action(obj: ESCObject, default_action: bool): current_action = obj.node.default_action_inventory else: current_action = obj.node.default_action - elif current_action in obj.node.combine_when_selected_action_is_in: + elif current_action in obj.node.actions_requiring_target_object: current_tool = obj tool_just_set = true return tool_just_set -## Checks if object requires a combination with another, according to currently selected action verb (or check with default action of the item).[br] +## Checks if the current tool requires a target object for the selected action verb.[br] ## [br] ## #### Parameters[br] ## [br] @@ -795,10 +768,10 @@ func _set_tool_and_action(obj: ESCObject, default_action: bool): ## #### Returns[br] ## [br] ## Returns a `bool` value. (`bool`) -func _check_item_needs_combine() -> bool: +func _check_item_requires_target_object() -> bool: return current_action \ and current_tool \ - and current_action in current_tool.node.combine_when_selected_action_is_in + and current_action in current_tool.node.actions_requiring_target_object ## Makes the player character walk towards the clicked item. Returns the resulting walk context.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/esc_command_registry.gd b/addons/escoria-core/game/core-scripts/esc/esc_command_registry.gd index fdea1ec..a5d5a3b 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_command_registry.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_command_registry.gd @@ -1,6 +1,6 @@ ## A registry of ASHES command objects. -extends RefCounted class_name ESCCommandRegistry +extends RefCounted ## The registry of registered commands. @@ -54,5 +54,4 @@ func load_command(command_name: String) -> ESCBaseCommand: func is_command_or_control_pressed(command_name: String) -> ESCBaseCommand: if self.registry.has(command_name): return self.registry[command_name] - else: - return self.load_command(command_name) + return self.load_command(command_name) diff --git a/addons/escoria-core/game/core-scripts/esc/esc_compiler.gd b/addons/escoria-core/game/core-scripts/esc/esc_compiler.gd index c029f47..5ed051f 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_compiler.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_compiler.gd @@ -1,6 +1,6 @@ ## Compiler for the ASHES language. -extends Resource class_name ESCCompiler +extends Resource ## This must match `ESCProjectSettingsManager.COMMAND_DIRECTORIES`. @@ -140,10 +140,21 @@ func _compiler_shim(source: String, filename: String = "", associated_global_id: if not had_error: for ps in parsed_statements: - var event_name = ps.get_event_name() - if ps.get_target() != null: - event_name += " " + ps.get_target_name() - script.events[event_name] = ps + if script.has_event_with_target( + ps.get_event_name(), + ps.get_target_name() + ): + ESCSafeLogging.log_error( + self, + "Duplicate event '%s' with target '%s'." % [ + ps.get_event_name(), + ps.get_target_name() + ] + ) + had_error = true + continue + + script.events.add(ps) return script @@ -184,7 +195,7 @@ func load_esc_file(path: String, associated_global_id: String = "") -> ESCScript ## #### Returns[br] ## [br] ## Returns a `ESCScript` value. (`ESCScript`) -func compile(script: String, path: String = "") -> ESCScript: +func compile(script: String, _path: String = "") -> ESCScript: return _compiler_shim(script) diff --git a/addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd index 47fda29..a1e35dc 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd @@ -4,8 +4,8 @@ ## the foreground channel _front, but additional event queues can be added as required. ## Additionally, events can be scheduled to be queued in the future. ## @MANAGER -extends Node class_name ESCEventManager +extends Node ## Emitted when the event has begun execution.[br] @@ -111,18 +111,15 @@ var events_queue: Dictionary = { ## Currently running event in background channels. var _running_events: Dictionary = {} +## Runtime interpreters currently owned by the event manager, keyed by channel. +var _runtime_interpreters: Dictionary = {} + ## Those commands that are currently running per channel. var _running_commands: Dictionary = {} -## Channel currently being processed. -var _current_channel: String = "" - ## Whether an event can be played on a specific channel. var _channels_state: Dictionary = {} -## Whether we're currently waiting for an async event to complete, per channel. -var _yielding: Dictionary = {} - ## Whether we're currently changing the scene. var _changing_scene: bool = false: set = set_changing_scene @@ -171,15 +168,10 @@ func _ready(): ## [br] ## Returns nothing. func _process(delta: float) -> void: - var channel_yielding: bool - for channel_name in events_queue.keys(): - channel_yielding = _yielding.get(channel_name, false) - - if events_queue[channel_name].size() == 0 or channel_yielding: + if events_queue[channel_name].size() == 0: continue if is_channel_free(channel_name): - _current_channel = channel_name _channels_state[channel_name] = false _running_events[channel_name] = \ events_queue[channel_name].pop_front() @@ -193,15 +185,15 @@ func _process(delta: float) -> void: if not _running_events[channel_name].source.is_empty() else "(unknown)" ) - if not _running_events[channel_name].is_connected( - "finished", _on_event_finished + if not _running_events[channel_name].finished.is_connected( + _on_event_finished.bind(channel_name) ): _running_events[channel_name].finished.connect( _on_event_finished.bind(channel_name), CONNECT_ONE_SHOT ) - if not _running_events[channel_name].is_connected( - "interrupted", _on_event_finished + if not _running_events[channel_name].interrupted.is_connected( + _on_event_finished.bind(channel_name) ): _running_events[channel_name].interrupted.connect( _on_event_finished.bind(channel_name), @@ -231,27 +223,9 @@ func _process(delta: float) -> void: #var rc = _running_events[channel_name].run() #escoria.interpreter.reset() #var resolver: ESCResolver = ESCResolver.new(escoria.interpreter) - var interpreter: ESCInterpreter = ESCInterpreterFactory.create_interpreter() - var resolver: ESCResolver = ESCResolver.new(interpreter) var event = _running_events[channel_name] - escoria.logger.trace( - self, - "Processing event '%s' from script '%s'." \ - % [ - event.get_name().get_lexeme(), - event.get_name().get_filename() if event.get_name().get_filename() else "<internal>" - ] - ) - - resolver.resolve(event) - - var rc = await interpreter.interpret(event) - - #if rc is GDScriptFunctionState: - #_yielding[channel_name] = true - #rc = await (rc, "completed") - #_yielding[channel_name] = false + call_deferred("_run_event_on_channel", channel_name, event) for event in self.scheduled_events: (event as ESCScheduledEvent).timeout -= delta @@ -260,6 +234,32 @@ func _process(delta: float) -> void: self.events_queue[CHANNEL_FRONT].append(event.event) +func _run_event_on_channel(channel_name: String, event: ESCGrammarStmts.Event) -> void: + var interpreter: ESCInterpreter = ESCInterpreterFactory.create_runtime_interpreter(channel_name) + _runtime_interpreters[channel_name] = interpreter + var resolver: ESCResolver = ESCResolver.new(interpreter) + + escoria.logger.trace( + self, + "Processing event '%s' from script '%s'." \ + % [ + event.get_name().get_lexeme(), + event.get_name().get_filename() if event.get_name().get_filename() else "<internal>" + ] + ) + + resolver.resolve(event) + await interpreter.interpret(event) + + # Runtime interpreters are owned per channel run. Clean up the local + # instance once interpretation has concluded without touching any newer + # interpreter another concurrent channel run may have registered. + interpreter.cleanup() + + if _runtime_interpreters.get(channel_name) == interpreter: + _runtime_interpreters.erase(channel_name) + + ## Queues a new event based on input from an ASHES command, most likely `queue_event`.[br] ## [br] ## #### Parameters[br] @@ -281,23 +281,24 @@ func queue_event_from_esc(script_object: ESCScript, event: String, return ESCExecution.RC_WONT_QUEUE if channel == CHANNEL_FRONT: - queue_event(script_object.events[event]) + queue_event(script_object.get_event_with_target(event)) else: queue_background_event( channel, - script_object.events[event] + script_object.get_event_with_target(event) ) + if block: if channel == CHANNEL_FRONT: var rc = await self.event_finished while rc[1] != event: rc = await self.event_finished return rc[0] - else: - var rc = await self.background_event_finished - while rc[1] != event and rc[2] != channel: - rc = await self.background_event_finished - return rc[0] + + var rc = await self.background_event_finished + while rc[1] != event or rc[2] != channel: + rc = await self.background_event_finished + return rc[0] return ESCExecution.RC_OK @@ -335,12 +336,13 @@ func queue_event(event: ESCGrammarStmts.Event, force: bool = false, as_first = f escoria.logger.debug(self, message % [event.get_event_name(), CHANNEL_FRONT]) return - elif _is_event_running(event, CHANNEL_FRONT): + + if _is_event_running(event, CHANNEL_FRONT): # Don't queue the same event if it's already running. escoria.logger.debug( self, "Event %s already running in channel '%s'. Won't be queued." - % [event.get_event_name(), CHANNEL_FRONT] + % [event.get_event_name(), CHANNEL_FRONT] ) return @@ -388,27 +390,10 @@ func queue_background_event(channel_name: String, event: ESCGrammarStmts.Event) if not channel_name in events_queue: events_queue[channel_name] = [] - # Don't queue the same event more than once in a row. - var last_event = _get_last_event_queued(channel_name) - - # Check the queue first to see if appending the event will result in - # consecutive occurrences of the event. If not, be sure to check if the same - # event is currently running. - if last_event != null and last_event.get_event_name() == event.get_event_name(): - var message = "Event '%s' is already the most-recently queued event in channel '%s'." + \ - " Won't be queued again." - - escoria.logger.debug(self, message % [event.get_event_name(), channel_name]) - return - elif _is_event_running(event, CHANNEL_FRONT): - # Don't queue the same event if it's already running. - escoria.logger.debug( - self, - "Event %s already running in channel '%s'. Won't be queued." - % [event.get_event_name(), channel_name] - ) - - return + # Background channels are fed by explicit ASHES commands, so repeated queue + # requests are intentional. This includes back-to-back enqueues of the same + # event name and enqueuing the same event again while an earlier run is + # already active on the channel. events_queue[channel_name].append(event) @@ -482,8 +467,23 @@ func interrupt(exceptions: PackedStringArray = [], stop_walking = true) -> void: ## [br] ## Returns nothing. func interrupt_channel(channel_name: String) -> void: - for command in _running_commands.get(channel_name, []): - command.interrupt() + var running_event: ESCGrammarStmts.Event = _running_events.get(channel_name, null) + + # Interrupt the channel's running event directly so any in-flight command is + # reached even if command bookkeeping has not yet been updated for this frame. + if running_event != null: + escoria.logger.debug( + self, + "Interrupting running event %s in channel %s..." + % [running_event.get_event_name(), channel_name] + ) + running_event.interrupt() + _channels_state[channel_name] = true + + # Clear any queued follow-up events on the same channel so the interrupt has + # the same effect as the broader interrupt path, but scoped to one channel. + if events_queue.has(channel_name): + events_queue[channel_name].clear() ## Clears the event queues.[br] @@ -563,10 +563,6 @@ func set_changing_scene(p_is_changing_scene: bool) -> void: ]) -# This probably won't work since _current_channel could have changed after -# a yielding command resumes. Also the event itself isn't logged, just the command, -# creating a problem. - ## Adds a currently-running command to the current channel.[br] ## [br] ## #### Parameters[br] @@ -579,10 +575,13 @@ func set_changing_scene(p_is_changing_scene: bool) -> void: ## [br] ## Returns nothing. func add_running_command(command: ESCCommand) -> void: - if _running_commands.get(_current_channel, []) == []: - _running_commands[_current_channel] = [command] + if command.channel_name.is_empty(): + return + + if _running_commands.get(command.channel_name, []) == []: + _running_commands[command.channel_name] = [command] else: - _running_commands[_current_channel].append(command) + _running_commands[command.channel_name].append(command) ## Removes the specified command from the current channel.[br] @@ -597,8 +596,11 @@ func add_running_command(command: ESCCommand) -> void: ## [br] ## Returns nothing. func running_command_finished(command: ESCCommand) -> void: - if command in _running_commands[_current_channel]: - _running_commands[_current_channel].erase(command) + if command.channel_name.is_empty(): + return + + if command in _running_commands.get(command.channel_name, []): + _running_commands[command.channel_name].erase(command) ## The event finished running.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd index 61c1e38..e9b86af 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd @@ -3,8 +3,8 @@ ## The ASHES global state is basically simply a dictionary of keys with ## values. Values can be bool, integer or strings. ## @MANAGER -extends Resource class_name ESCGlobalsManager +extends Resource ## Emitted when a global has changed.[br] @@ -123,6 +123,19 @@ func get_global(key: String): return null +## Returns a snapshot copy of all currently stored globals.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Dictionary` containing the current global state. (`Dictionary`) +func get_globals() -> Dictionary: + return _globals.duplicate(true) + + ## Filters 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.[br] ## [br] ## #### Parameters[br] diff --git a/addons/escoria-core/game/core-scripts/esc/esc_inventory_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_inventory_manager.gd index 944aead..a6016ce 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_inventory_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_inventory_manager.gd @@ -1,7 +1,7 @@ ## A manager for inventory objects. ## @MANAGER -extends Resource class_name ESCInventoryManager +extends Resource ## Checks whether the player has an inventory item.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd index 047fcab..de2e671 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd @@ -1,7 +1,7 @@ ## A manager for ESC objects. ## @MANAGER -extends Resource class_name ESCObjectManager +extends Resource ## Reserved camera object. @@ -94,6 +94,23 @@ func _init() -> void: current_room_key = ESCRoomObjectsKey.new() +## Clears all room-specific object and terrain state while preserving reserved +## objects managed by Escoria itself.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func reset_game_state() -> void: + room_objects.clear() + room_objects.push_back(reserved_objects_container) + room_terrains.clear() + current_room_key = ESCRoomObjectsKey.new() + + ## Updates which object manager room is to be treated as the currently active one.[br] ## [br] ## #### Parameters[br] @@ -133,7 +150,6 @@ func set_current_room(room: ESCRoom) -> void: ## Returns nothing. func register_object(object: ESCObject, room: ESCRoom = null, force: bool = false, \ auto_unregister: bool = true) -> void: - if object.global_id.is_empty(): object.global_id = str(object.node.get_path()).split("/root/", false)[0] object.node.global_id = object.global_id @@ -193,7 +209,7 @@ func register_object(object: ESCObject, room: ESCRoom = null, force: bool = fals return # Object exists in room, set it to is last state (if different from # "default") - elif object.node is ESCItem and _object_exists_in_room(object, room_key): + if object.node is ESCItem and _object_exists_in_room(object, room_key): # Object is already known, set its state to last known state object.set_state(get_object(object.global_id).state) @@ -201,16 +217,26 @@ func register_object(object: ESCObject, room: ESCRoom = null, force: bool = fals # forcing the registration, since we don't know if this object will be # overwritten ("forced") in the future and, if it is, if it's set to # auto-unregister or not. In most cases, objects are set to auto unregister. - if object.node.tree_exited.is_connected(unregister_object): - object.node.tree_exited.disconnect(unregister_object) + if not object.unregister_object_callback.is_null() and \ + object.node.tree_exited.is_connected(object.unregister_object_callback): + object.node.tree_exited.disconnect(object.unregister_object_callback) if force: + var existing_object: ESCObject = _get_room_objects_objects(room_key).get(object.global_id) + if existing_object != null and is_instance_valid(existing_object.node): + if existing_object.node.tree_exited.is_connected( + existing_object.unregister_object_callback + ): + existing_object.node.tree_exited.disconnect( + existing_object.unregister_object_callback + ) # If this ID already exists and we're about to overwrite it, do the # safe thing and unregister the old object first unregister_object_by_global_id(object.global_id, room_key) if auto_unregister: - object.node.tree_exited.connect(unregister_object.bind(object, room_key)) + object.unregister_object_callback = unregister_object.bind(object, room_key) + object.node.tree_exited.connect(object.unregister_object_callback) if "is_interactive" in object.node and object.node.is_interactive: object.interactive = true @@ -352,13 +378,13 @@ func get_object(global_id: String, room: ESCRoom = null) -> ESCObject: if global_id in RESERVED_OBJECTS: if reserved_objects_container.objects.has(global_id): return reserved_objects_container.objects[global_id] - else: - escoria.logger.warn( - self, - "Reserved object with global id %s not found in object manager!" - % global_id - ) - return null + + escoria.logger.warn( + self, + "Reserved object with global id %s not found in object manager!" + % global_id + ) + return null var room_key: ESCRoomObjectsKey @@ -382,24 +408,24 @@ func get_object(global_id: String, room: ESCRoom = null) -> ESCObject: if objects.has(global_id): return objects[global_id] - else: - escoria.logger.warn( - self, - "Object with global id %s in room instance (%s, %s) not found. This can be safely ignored if a room was being searched for." - % [global_id, room_key.room_global_id, room_key.room_instance_id] - ) - if escoria.inventory_manager.inventory_has(global_id): - # item is in the inventory and may be registered to a different room - for single_room in room_objects: - # these are arrays of the objects still registered for each room - if single_room.objects.has(global_id): - escoria.logger.info( - self, - "Object with global id %s found in room instance (%s, %s) through the inventory." - % [global_id, room_key.room_global_id, room_key.room_instance_id] - ) - return single_room.objects[global_id] - return null + + escoria.logger.warn( + self, + "Object with global id %s in room instance (%s, %s) not found. This can be safely ignored if a room was being searched for." + % [global_id, room_key.room_global_id, room_key.room_instance_id] + ) + if escoria.inventory_manager.inventory_has(global_id): + # item is in the inventory and may be registered to a different room + for single_room in room_objects: + # these are arrays of the objects still registered for each room + if single_room.objects.has(global_id): + escoria.logger.info( + self, + "Object with global id %s found in room instance (%s, %s) through the inventory." + % [global_id, room_key.room_global_id, room_key.room_instance_id] + ) + return single_room.objects[global_id] + return null ## Removes an object from the registry.[br] @@ -422,7 +448,7 @@ func unregister_object(object: ESCObject, room_key: ESCRoomObjectsKey) -> void: escoria.logger.debug( self, "Unable to unregister object. " + - "Object with global ID %s room (%s, %s) not found. If this was " + "Object with global ID '%s' room (%s, %s) not found. If this was " % [ "?" if object == null else object.global_id, room_key.room_global_id, @@ -430,7 +456,6 @@ func unregister_object(object: ESCObject, room_key: ESCRoomObjectsKey) -> void: ] + "part of a 'forced' registration, ignore this warning." ) - return var room_objects = _get_room_objects_objects(room_key) @@ -701,3 +726,31 @@ func _erase_room(room_key: ESCRoomObjectsKey) -> void: if _compare_container_to_key(room_container, room_key): room_objects.erase(room_container) return + + +## Disconnect all existing items from registered ESCObjects for the given room. +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |room_key|`ESCRoomObjectsKey`|The key representing the desired room in the object manager array|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func disconnect_tree_exit_for_room_items(room_key: ESCRoomObjectsKey) -> void: + #var _room_objects = _get_room_objects_objects(room_key) + var i: int = 0 + for room_container in room_objects: + if room_container.room_global_id == room_key.room_global_id \ + and room_container.room_instance_id == room_key.room_instance_id: + for object in room_container.objects: + var ro: ESCObject = room_objects[i].objects[object] + if is_instance_valid(ro.node) and \ + ro.node.tree_exited.is_connected(ro.unregister_object_callback): + ro.node.tree_exited.disconnect(ro.unregister_object_callback) + break + else: + i += 1 + continue diff --git a/addons/escoria-core/game/core-scripts/esc/esc_room_manager.gd b/addons/escoria-core/game/core-scripts/esc/esc_room_manager.gd index 1a077a9..60042f7 100644 --- a/addons/escoria-core/game/core-scripts/esc/esc_room_manager.gd +++ b/addons/escoria-core/game/core-scripts/esc/esc_room_manager.gd @@ -1,7 +1,7 @@ ## A manager for rooms in a game. ## @MANAGER -extends Resource class_name ESCRoomManager +extends Resource ## The room manager scene. ## Manages room loading and keeps in memory @@ -116,6 +116,23 @@ func change_scene_to_file(room_path: String, enable_automatic_transitions: bool) room_scene.add_child(escoria.game_scene) room_scene.move_child(escoria.game_scene, 0) room_scene.game = escoria.game_scene + + # If the scene we're loading is the same as the current one, disconnect + # the tree_exit signal on the existing items (that was connected when + # items got registered in Object Manager) so that objects for new items + # of the new room don't get unregistered. + var current_scene: Node = escoria.main.current_scene + var new_room_global_id: String = room_scene.global_id \ + if not room_scene.global_id.is_empty() else room_scene.name + if is_instance_valid(current_scene): + var current_room_global_id: String = \ + current_scene.global_id if not current_scene.global_id.is_empty() \ + else current_scene.name + if new_room_global_id == current_room_global_id: + escoria.object_manager.disconnect_tree_exit_for_room_items( + escoria.object_manager.current_room_key + ) + escoria.main.set_scene(room_scene) # We know the scene has been loaded. Make its global ID available for @@ -341,7 +358,7 @@ func _perform_script_events(room: ESCRoom) -> int: get_class()) escoria.event_manager.queue_event( - script_transition_out_compiled.events[escoria.event_manager.EVENT_TRANSITION_OUT], + script_transition_out_compiled.get_event_with_target(escoria.event_manager.EVENT_TRANSITION_OUT), true ) @@ -381,14 +398,6 @@ func _perform_script_events(room: ESCRoom) -> int: if not is_instance_valid(room.player): room.player = room.player_scene.instantiate() room.add_child(room.player) - escoria.object_manager.register_object( - ESCObject.new( - room.player.global_id, - room.player - ), - room, - true - ) if escoria.globals_manager.has( escoria.room_manager.GLOBAL_ANIMATION_RESOURCES @@ -424,12 +433,19 @@ func _perform_script_events(room: ESCRoom) -> int: # Add new camera to scene being prepared. if room.player_camera: - room.remove_child(room.player_camera) + room.remove_child(room.player_camera) # This unregisters the camera from object manager room.player_camera.queue_free() var new_player_camera: ESCCamera = escoria.resource_cache.get_resource( CAMERA_SCENE_PATH ).instantiate() - new_player_camera.register() + escoria.object_manager.register_object( + ESCObject.new( + ESCObjectManager.CAMERA, + new_player_camera + ), + null, + true + ) room.player_camera = new_player_camera # We must first set the camera limits, and then worry about subsequent @@ -530,10 +546,10 @@ func _perform_script_events(room: ESCRoom) -> int: var script_transition_in = escoria.esc_compiler.compile(transition_in_script.build()) escoria.event_manager.queue_event( - script_transition_in.events[escoria.event_manager.EVENT_TRANSITION_IN] + script_transition_in.get_event_with_target(escoria.event_manager.EVENT_TRANSITION_IN) ) - if not escoria.current_state == escoria.GAME_STATE.LOADING: + if not escoria.current_state == escoria.GameState.LOADING: var ready_event_added: bool = false # Run the ready event, if there is one. ready_event_added = _run_script_event(escoria.event_manager.EVENT_READY, room) @@ -593,14 +609,15 @@ func _run_script_event(event_name: String, room: ESCRoom): room.compiled_script = \ escoria.esc_compiler.load_esc_file(room.esc_script) - if room.compiled_script.events.has(event_name): + if room.compiled_script.has_event_with_target(event_name): + var event = room.compiled_script.get_event_with_target(event_name) escoria.logger.debug( self, "Queuing room script event %s " % event_name + "composed of %s statements." - % room.compiled_script.events[event_name].get_num_statements_in_block() + % event.get_num_statements_in_block() ) - escoria.event_manager.queue_event(room.compiled_script.events[event_name], true) + escoria.event_manager.queue_event(event, true) return true - else: - return false + + return false diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_base_command.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_base_command.gd index c46714d..e7f27e5 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_base_command.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_base_command.gd @@ -1,8 +1,8 @@ ## Abstract base class for every ESC command. ## ## Extending classes have to override the configure and run function -extends Resource class_name ESCBaseCommand +extends Resource ## The filename from which the relevant command is being called, if available. @@ -11,6 +11,12 @@ var filename: String = "" ## The line number from the file the relevant command is being called from. var line_number: int = 0 +## Whether the command is allowed to be ran in a room's :ready event during a +## savegame loading. This is especially the case for sound control commands, +## for which the Save Manager resumes from the recorded position.[br] +## Set this to `false` in the `_init()` method of such commands when necessary. +var run_in_ready_allowed_during_savegame_loading: bool = true + ## A descriptor (contained in `ESCCommandArgumentDescriptor`) of the arguments of this command.[br] ## [br] @@ -60,7 +66,7 @@ func validate(arguments: Array) -> bool: ## #### Returns[br] ## [br] ## Returns a `int` value. (`int`) -func run(command_params: Array) -> int: +func run(_command_params: Array) -> int: raise_error(self, "Command %s did not override run. Please implement a run() function." % get_command_name()) return 0 diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_camera_base_command.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_camera_base_command.gd index 54af190..88088a8 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_camera_base_command.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_camera_base_command.gd @@ -6,8 +6,8 @@ ## [br] ## None. ## [br] -extends ESCBaseCommand class_name ESCCameraBaseCommand +extends ESCBaseCommand ## Generates a log entry when attempting to move the camera to an invalid position.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_command.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_command.gd index 4a5cb76..1008a14 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_command.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_command.gd @@ -1,8 +1,8 @@ ## Used to properly setup and execute commands, i.e. those that extend `ESCBaseCommand`. ## ## Despite its name, `ESCCommand` is more of a container and orchestrator than an actual command. -extends ESCStatement class_name ESCCommand +extends ESCStatement ## The name of this command. @@ -11,10 +11,15 @@ var name: String ## Parameters for this command var parameters: Array = [] +## Channel where this command's owning event is running. +var channel_name: String = "" + ## The token generated by the parser containing info for debugging (e.g. line number, ## script filename, etc.). var parser_token: ESCToken = null +var _running_command_impl: ESCBaseCommand = null + ## A `Dictionary` containing the data needed when saving a game.[br] ## [br] @@ -85,36 +90,50 @@ func _command_exists() -> bool: ## Returns a `int` value. (`int`) func run() -> int: var command_object = escoria.command_registry.load_command(self.name) + _running_command_impl = command_object _decorate_for_debugging(command_object) if command_object == null: + _running_command_impl = null return ESCExecution.RC_ERROR - else: - var argument_descriptor = command_object.configure() - _decorate_for_debugging(argument_descriptor) + var argument_descriptor = command_object.configure() - var prepared_arguments = argument_descriptor.prepare_arguments( - self.parameters - ) + _decorate_for_debugging(argument_descriptor) + + var prepared_arguments = argument_descriptor.prepare_arguments( + self.parameters + ) - if command_object.validate(prepared_arguments): + if command_object.validate(prepared_arguments): + if escoria.save_manager.is_loading_game \ + and escoria.event_manager.get_running_event(escoria.event_manager.CHANNEL_FRONT).get_event_name() == escoria.event_manager.EVENT_READY \ + and not command_object.run_in_ready_allowed_during_savegame_loading: escoria.logger.debug( self, - "Running command %s with parameters %s." + "Skipped command %s with parameters %s because it is not allowed to be ran from :ready event during savegame load." % [self.name, prepared_arguments] ) - escoria.event_manager.add_running_command(self) - var rc = await command_object.run(prepared_arguments) - escoria.event_manager.running_command_finished(self) - escoria.logger.debug( - self, - "[%s] Return code: %d." % [self.name, rc] - ) - return rc - else: - return ESCExecution.RC_ERROR + _running_command_impl = null + return ESCExecution.RC_WONT_QUEUE + escoria.logger.debug( + self, + "Running command %s with parameters %s." + % [self.name, prepared_arguments] + ) + escoria.event_manager.add_running_command(self) + var rc = await command_object.run(prepared_arguments) + escoria.event_manager.running_command_finished(self) + _running_command_impl = null + escoria.logger.debug( + self, + "[%s] Return code: %d." % [self.name, rc] + ) + return rc + + _running_command_impl = null + return ESCExecution.RC_ERROR ## This function interrupts the command. If it was not started, it will not run. If it had already started, the execution will be considered as finished immediately and finish. If it had already finished, nothing will happen.[br] @@ -128,9 +147,8 @@ func run() -> int: ## Returns nothing. func interrupt(): _is_interrupted = true - var command = escoria.command_registry.is_command_or_control_pressed(self.name) - if command.has_method("interrupt"): - command.interrupt() + if _running_command_impl and _running_command_impl.has_method("interrupt"): + _running_command_impl.interrupt() ## Override of built-in _to_string function to display the statement.[br] ## [br] diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_command_argument_descriptor.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_command_argument_descriptor.gd index 44b3b77..8367f49 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_command_argument_descriptor.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_command_argument_descriptor.gd @@ -1,6 +1,6 @@ ## Describes the arguments of commands that extend `ESCBaseCommand`. -extends RefCounted class_name ESCCommandArgumentDescriptor +extends RefCounted ## As the get_type command was deprecated with Godot 2.x w we need a way to determine diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_dialog.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_dialog.gd index 5d48a98..032696f 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_dialog.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_dialog.gd @@ -1,6 +1,6 @@ ## Represents a dialog in Escoria. -extends ESCStatement class_name ESCDialog +extends ESCStatement ## Avatar used in the dialog, if any. @@ -12,6 +12,9 @@ var timeout: int = 0 ## The dialog option to select when timeout is reached. var timeout_option: int = 0 +## Total number of options authored in this dialog, including currently hidden options. +var authored_options_count: int = 0 + ## A list of `ESCDialogOption`s. var options: Array @@ -32,7 +35,11 @@ func is_valid() -> bool: "Avatar scene not found: %s." % self.avatar ) return false - if self.timeout_option > self.options.size() \ + var option_count := self.authored_options_count + if option_count == 0: + option_count = self.options.size() + + if self.timeout_option > option_count \ or self.timeout_option < 0: escoria.logger.error( self, @@ -43,6 +50,33 @@ func is_valid() -> bool: return true +## Returns the currently visible option configured as the timeout default, if any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the currently visible timeout option as an `ESCDialogOption`, or `null` if the configured option is not visible. +func get_timeout_option() -> ESCDialogOption: + var has_source_option_indexes := false + + for option in self.options: + if option.source_option_index > 0: + has_source_option_indexes = true + + if option.source_option_index == self.timeout_option: + return option + + if not has_source_option_indexes \ + and self.timeout_option > 0 \ + and self.timeout_option <= self.options.size(): + return self.options[self.timeout_option - 1] + + return null + + ## Run this dialog. TODO: Although this method overrides its parent version, the return type here does NOT match the parent's signature. Consider either changing the parent's return type to be a `Variant`, or doing something to ensure greater consistency.[br] ## [br] ## #### Parameters[br] @@ -58,7 +92,7 @@ func run(): "Starting dialog." ) - escoria.current_state = escoria.GAME_STATE.DIALOG + escoria.current_state = escoria.GameState.DIALOG if !escoria.dialog_player: escoria.dialog_player = escoria.main.current_scene.get_node( @@ -75,6 +109,6 @@ func run(): # If this is the case and the current level of dialog has a parent, it means # it is still yielding and so will be shown again. - escoria.current_state = escoria.GAME_STATE.DEFAULT + escoria.current_state = escoria.GameState.DEFAULT return option diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_dialog_option.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_dialog_option.gd index e3701e5..8737dbc 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_dialog_option.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_dialog_option.gd @@ -1,17 +1,23 @@ ## A single option used as part of a dialog. ## ## `ESCDialog` makes use of these when assembling an actual dialog in Escoria. -extends ESCStatement class_name ESCDialogOption +extends ESCStatement ## Option text displayed in the HUD. var option: String: get = get_translated_option +## Explicit translation key for the option text. +var translation_key: String = "" + ## Maps back to the parsed source option. var source_option +## One-based index of this option in its authored dialog block. +var source_option_index: int = -1 + ## Whether this option is valid. var _is_valid: bool: set = set_is_valid, @@ -28,6 +34,14 @@ var _is_valid: bool: ## [br] ## Returns the translated version of the option, if one exists; otherwise, the default text is returned. (`String`) func get_translated_option() -> String: + if not translation_key.is_empty(): + var translated_text = tr(translation_key) + + if translation_key != translated_text: + return translated_text + + return option + # Check if text has a key if ":" in option: var splitted_text = option.split(":") diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_event.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_event.gd index 19a0075..bf9e92c 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_event.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_event.gd @@ -12,18 +12,8 @@ ## run once the room is visible.[br] ## - `:use <global id>` : Called from the current item when it is used with the item ## with specified by `<global id>`. -extends ESCStatement class_name ESCEvent - -## Regex identifying an ESC event. -const REGEX = \ - '^:(?<name>[^|]+)( \\|\\s*(?<flags>( ' + \ - '(TK|NO_TT|NO_UI|NO_SAVE)' + \ - ')+))?$' - -## Prefix to identify this as an ESC event. -const PREFIX = ":" - +extends ESCStatement ## Valid event flags:[br] ## - `TK`: stands for "telekinetic". It means the player won't walk over to @@ -43,6 +33,14 @@ enum FLAGS { NO_SAVE = 8 } +## Regex identifying an ESC event. +const REGEX = \ + '^:(?<name>[^|]+)( \\|\\s*(?<flags>( ' + \ + '(TK|NO_TT|NO_UI|NO_SAVE)' + \ + ')+))?$' + +## Prefix to identify this as an ESC event. +const PREFIX = ":" ## Name of the event. var name: String diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd new file mode 100644 index 0000000..0ef313e --- /dev/null +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd @@ -0,0 +1,52 @@ +## Container for storing events in Escoria, providing a consistent way to provide +## visibility and access to events. +class_name ESCEventsContainer +extends RefCounted + + +## The list of events being tracked. +var events: Array = [] + + +## Adds an event to the container.[br] +##[br] +## #### Parameters[br] +##[br] +## - event: the event to be stored +func add(event) -> void: + events.append(event) + + +## Determines whether the specified events list contains an event with the[br] +## specified event name and event target, e.g. `:give "filled_out_form"`[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* true iff events contains an event matching both event_name and event_target +func has_event_with_target(event_name: String, event_target = null) -> bool: + return is_instance_valid(get_event_with_target(event_name, event_target)) + + +## Returns the event matching the specified event name and target, e.g. `:give "filled_out_form"`[br] +## if it exists; returns null otherwise.[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* the event in `events` iff `events` contains an event matching both `event_name` and `event_target`;[br] +## returns null otherwise. +func get_event_with_target(event_name: String, event_target = null): + for event in events: + if event.get_event_name() == event_name: + if event_target: + if event.get_target_name() == event_target: + return event + else: + if not event.get_target_name(): + return event + return null diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd.uid b/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd.uid new file mode 100644 index 0000000..2559148 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd.uid @@ -0,0 +1 @@ +uid://ct6ek3jk6spbq diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_execution.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_execution.gd index 21641e7..917895c 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_execution.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_execution.gd @@ -1,6 +1,6 @@ ## Class with enum containing the return codes returned by ASHES script functions. -extends Resource class_name ESCExecution +extends Resource ## Execution result values. ## Return codes handled by events.[br] diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_object.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_object.gd index 64d3596..eb975ae 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_object.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_object.gd @@ -1,7 +1,10 @@ ## Represents an object that is able to be handled by Escoria. -extends RefCounted class_name ESCObject +extends RefCounted +const ESC_EVENTS_CONTAINER_SCRIPT := preload( + "res://addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd" +) ## Default object state. const STATE_DEFAULT: String = "default" @@ -21,12 +24,15 @@ var interactive: bool = true: get = _get_interactive, set = _set_interactive var state: String = STATE_DEFAULT: get = get_state -## The events registered with the object. -var events: Dictionary = {} +## The events registered with the object +var events = ESC_EVENTS_CONTAINER_SCRIPT.new() ## The node representing the object in the scene. var node: Node +## Variable that stores the Callable method to unregister the object from Object Manager +var unregister_object_callback: Callable + ## Initializes the ESCObject with a global id and node.[br] ## [br] ## #### Parameters[br] @@ -142,8 +148,8 @@ func is_active() -> bool: func _get_interactive() -> bool: if is_instance_valid(self.node) and "is_interactive" in self.node: return self.node.is_interactive - else: - return true + + return true ## Sets the interactive value in the node.[br] @@ -177,7 +183,7 @@ func _set_interactive(value: bool): func get_save_data() -> Dictionary: var save_data: Dictionary = {} - if self.global_id == "player" and not is_instance_valid(self.node): + if self.node is ESCPlayer and not is_instance_valid(self.node): return save_data save_data["active"] = self.active @@ -185,18 +191,59 @@ func get_save_data() -> Dictionary: save_data["state"] = self.state if is_instance_valid(self.node): + if self.node is ESCItem: + if self.node.animations != null: + save_data["animations_resource_path"] = (self.node as ESCItem).animations.resource_path + var animation_player = (self.node as ESCItem).get_animation_player() + if is_instance_valid(animation_player): + save_data["current_animation"] = animation_player.get_current_animation() + if self.node.get("is_movable") and self.node.is_movable: save_data["global_transform"] = self.node.global_transform save_data["last_deg"] = wrapi(self.node._movable._get_angle() - 90 + 1, 0, 360) save_data["last_dir"] = self.node._movable.last_dir + save_data["speed"] = self.node.speed if self.node.has_method("get_custom_data"): save_data["custom_data"] = self.node.get_custom_data() - if self.global_id in ["_music", "_sound", "_ambient"] and self.node.get("state"): + if self.global_id in [ESCObjectManager.MUSIC, ESCObjectManager.SOUND, ESCObjectManager.AMBIENT] and self.node.get("state"): save_data["state"] = self.node.get("state") - save_data["playback_position"] = self.node.get_playback_position() + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.SAVE_SOUNDS_PLAYBACK_POSITION + ): + save_data["playback_position"] = self.node.get_playback_position() - if self.global_id == "_camera": + if self.global_id == ESCObjectManager.CAMERA: save_data["target"] = self.node.get("_follow_target").global_id + var camera_limit_id = escoria.main.get_camera_limit_id_for_room(escoria.main.current_scene) + if camera_limit_id != null: + save_data["limit_id"] = camera_limit_id return save_data + + +## Determines whether the specified events list contains an event with the[br] +## specified event name and event target, e.g. `:give "filled_out_form"`[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* true iff events contains an event matching both event_name and event_target +func has_event_with_target(event_name: String, event_target = null) -> bool: + return events.has_event_with_target(event_name, event_target) + + +## Returns the event matching the specified event name and target, e.g. `:give "filled_out_form"`[br] +## if it exists; returns null otherwise.[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* the event in `events` iff `events` contains an event matching both `event_name` and `event_target`;[br] +## returns null otherwise. +func get_event_with_target(event_name: String, event_target = null): + return events.get_event_with_target(event_name, event_target) diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_room_container.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_room_container.gd index 591d2b5..4c5bb80 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_room_container.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_room_container.gd @@ -1,7 +1,7 @@ ## Abstract base class for a container of Escoria entities specific to a room ## to be stored in and used by the Escoria object manager. -extends RefCounted class_name ESCRoomContainer +extends RefCounted ## Designates whether the objects contained in this container are all reserved objects. diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects.gd index 4a617ce..e798e8e 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects.gd @@ -1,6 +1,6 @@ ## Container for a room's `ESCObject`s stored in the object manager. -extends ESCRoomContainer class_name ESCRoomObjects +extends ESCRoomContainer ## The hash of objects registered to the room. diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects_key.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects_key.gd index b6f5ce4..0d303d0 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects_key.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_room_objects_key.gd @@ -1,7 +1,7 @@ ## Simple pair container to store a room's identifying information for use in ## the object manager. -extends RefCounted class_name ESCRoomObjectsKey +extends RefCounted ## Contains the `global_id` of the room being represented by this key. diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_room_terrains.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_room_terrains.gd index 153a49c..cefac5b 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_room_terrains.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_room_terrains.gd @@ -1,6 +1,6 @@ ## Container for `NavigationPolygon`s stored in the object manager. -extends ESCRoomContainer class_name ESCRoomTerrains +extends ESCRoomContainer ## The hash of terrains registered to the room. diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_scheduled_event.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_scheduled_event.gd index 736f241..8053366 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_scheduled_event.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_scheduled_event.gd @@ -1,6 +1,6 @@ ## Represents an event that is scheduled to run sometime in the future. -extends RefCounted class_name ESCScheduledEvent +extends RefCounted ## The event to run when timeout is reached. diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_script.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_script.gd index efca0f8..ba5b7d9 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_script.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_script.gd @@ -1,11 +1,49 @@ ## Represents a compiled ASHES script. -extends Resource class_name ESCScript +extends Resource + +const ESC_EVENTS_CONTAINER_SCRIPT := preload( + "res://addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd" +) + +## The events registered with the script. +var events = ESC_EVENTS_CONTAINER_SCRIPT.new() + +############## +# New interpreter stuff +var parsed_events = [] -## A dictionary of `ESCEvent`s contained in this script. -var events: Dictionary ## The name of the ASHES file parsed for this script, if this script was in fact ## loaded from a file (as opposed to being from an internal string). var filename: String = "" + +############## + + +## Determines whether the specified events list contains an event with the[br] +## specified event name and event target, e.g. `:give "filled_out_form"`[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* true iff events contains an event matching both event_name and event_target +func has_event_with_target(event_name: String, event_target = null) -> bool: + return events.has_event_with_target(event_name, event_target) + + +## Returns the event matching the specified event name and target, e.g. `:give "filled_out_form"`[br] +## if it exists; returns null otherwise.[br] +##[br] +## #### Parameters[br] +##[br] +## - event_name: the event name to search for[br] +## - event_target: the target for the specified event to check; may be null[br] +##[br] +## *Returns* the event in `events` iff `events` contains an event matching both `event_name` and `event_target`;[br] +## returns null otherwise. +func get_event_with_target(event_name: String, event_target = null): + return events.get_event_with_target(event_name, event_target) diff --git a/addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd b/addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd index 345dd28..5a8e800 100644 --- a/addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd +++ b/addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd @@ -5,8 +5,8 @@ ## newer ASHES scripting language used by Escoria. Although this class is extended ## to facilitate the execution of actual commands and dialogs, the name may be ## a bit misleading and may eventually be renamed or refactored. -extends RefCounted class_name ESCStatement +extends RefCounted ## Emitted when the event has finished running.[br] @@ -136,7 +136,7 @@ func run() -> int: ) if rc == ESCExecution.RC_REPEAT: return await self.run() - elif rc != ESCExecution.RC_OK: + if rc != ESCExecution.RC_OK: final_rc = rc current_statement.is_completed = true break diff --git a/addons/escoria-core/game/core-scripts/esc_animation_player.gd b/addons/escoria-core/game/core-scripts/esc_animation_player.gd index 66b908e..807d6c1 100644 --- a/addons/escoria-core/game/core-scripts/esc_animation_player.gd +++ b/addons/escoria-core/game/core-scripts/esc_animation_player.gd @@ -1,7 +1,7 @@ ## An abstraction class to expose the same animation methods for both ## AnimatedSprite and AnimationPlayer. -extends Node class_name ESCAnimationPlayer +extends Node ## Signal emitted when the animation finished playing.[br] @@ -28,7 +28,8 @@ var _animated_sprite: AnimatedSprite2D var _is_animation_player: bool = false ## Currently running animation. -var _current_animation: String = "" +var _current_animation: String = "": + get = get_current_animation ## Create a new animation player.[br] @@ -80,8 +81,7 @@ func _ready() -> void: func get_animation() -> String: if _is_animation_player: return _animation_player.assigned_animation - else: - return _animated_sprite.animation + return _animated_sprite.animation ## A list of all animation names.[br] @@ -96,8 +96,7 @@ func get_animation() -> String: func get_animations() -> PackedStringArray: if _is_animation_player: return _animation_player.get_animation_list() - else: - return _animated_sprite.sprite_frames.get_animation_names() + return _animated_sprite.sprite_frames.get_animation_names() ## Whether the animation is playing.[br] @@ -187,8 +186,7 @@ func play_backwards(name: String): func has_animation(name: String) -> bool: if _is_animation_player: return _animation_player.has_animation(name) - else: - return _animated_sprite.sprite_frames.has_animation(name) + return _animated_sprite.sprite_frames.has_animation(name) ## Play an animation and directly skip to the end.[br] @@ -203,6 +201,7 @@ func has_animation(name: String) -> bool: ## [br] ## Returns nothing. func seek_end(name: String): + _current_animation = name if _is_animation_player: _animation_player.current_animation = name _animation_player.seek(_animation_player.get_animation(name).length, true) @@ -225,9 +224,9 @@ func seek_end(name: String): func get_length(name: String) -> float: if _is_animation_player: return _animation_player.get_animation(name).length - else: - return _animated_sprite.sprite_frames.get_frame_count(name) - 1 * \ - _animated_sprite.sprite_frames.get_animation_speed(name) + + return _animated_sprite.sprite_frames.get_frame_count(name) - 1 * \ + _animated_sprite.sprite_frames.get_animation_speed(name) ## Return true if the ESCAnimationPlayer node is valid, ie. it has a valid player node.[br] @@ -257,7 +256,7 @@ func is_valid() -> bool: func _on_animation_finished(name: String): if _is_animation_player and not _animation_player.get_animation(name).loop_mode != Animation.LOOP_NONE: _animation_player.stop(true) # param here is to keep current state and - # avoid resetting animation position to 0 + # avoid resetting animation position to 0 elif not _animated_sprite.sprite_frames.get_animation_loop(name): _animated_sprite.stop() animation_finished.emit(name) @@ -274,3 +273,16 @@ func _on_animation_finished(name: String): ## Returns nothing. func _on_animation_finished_animated_sprite(): _on_animation_finished(_current_animation) + + +## Gets the current animation name.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## A string containing the current animation name. +func get_current_animation() -> String: + return _current_animation diff --git a/addons/escoria-core/game/core-scripts/esc_background.gd b/addons/escoria-core/game/core-scripts/esc_background.gd index fff871d..e0fb6d2 100644 --- a/addons/escoria-core/game/core-scripts/esc_background.gd +++ b/addons/escoria-core/game/core-scripts/esc_background.gd @@ -10,8 +10,8 @@ ## is set over the whole scene, because its rect_size is then used to create the ## Area2D node under it. If the rect_size is wrongly set, the background may ## receive no input. -extends TextureRect class_name ESCBackground +extends TextureRect ## The background was double clicked[br] @@ -137,7 +137,7 @@ func _ready(): ## [br] ## Returns nothing. func _unhandled_input(event: InputEvent) -> void: - var is_default_state = escoria.current_state == escoria.GAME_STATE.DEFAULT + var is_default_state = escoria.current_state == escoria.GameState.DEFAULT if escoria.inputs_manager.try_custom_input_handler(event, is_default_state): return if not is_default_state: diff --git a/addons/escoria-core/game/core-scripts/esc_dialog_location.gd b/addons/escoria-core/game/core-scripts/esc_dialog_location.gd index 0d5c7c1..80c38f0 100644 --- a/addons/escoria-core/game/core-scripts/esc_dialog_location.gd +++ b/addons/escoria-core/game/core-scripts/esc_dialog_location.gd @@ -5,8 +5,8 @@ ## automatically use an `ESCLocation` that is a child of the destination node. ## Commands like `turn_to`--which are not movement-based--will ignore child ## `ESCLocation`s and refer to the parent node. -extends ESCLocation class_name ESCDialogLocation +extends ESCLocation ## Whether this object's class is the same as given string class name.[br] @@ -15,10 +15,10 @@ class_name ESCDialogLocation ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |p_classname|`String`|String class to compare against|yes|[br] +## |p_classname|`String (4.6.x), StringName (4.7.x)`|Class name to compare against this location.|yes|[br] ## [br] ## #### Returns[br] ## [br] ## Returns a `bool` value. (`bool`) -func is_class(p_classname: String) -> bool: +func is_class(p_classname) -> bool: return p_classname == "ESCDialogLocation" diff --git a/addons/escoria-core/game/core-scripts/esc_exit.gd b/addons/escoria-core/game/core-scripts/esc_exit.gd index 79db99a..b9851c5 100644 --- a/addons/escoria-core/game/core-scripts/esc_exit.gd +++ b/addons/escoria-core/game/core-scripts/esc_exit.gd @@ -11,8 +11,8 @@ ## [br] ## The game character will automatically walk to an ``ESCLocation`` created as a ## child of an ``ESCExit`` node. -extends ESCItem class_name ESCExit +extends ESCItem ## Path to the target scene to change to @@ -72,9 +72,9 @@ func _ready(): ## Returns nothing. func _register_event(): if escoria.object_manager.has(self.global_id) and\ - not escoria.event_manager.EVENT_EXIT_SCENE in escoria.object_manager.get_object( + not escoria.object_manager.get_object( self.global_id - ).events: + ).has_event_with_target(escoria.event_manager.EVENT_EXIT_SCENE): var exit_scene_script_builder: ESCScriptBuilder = ESCScriptBuilder.new() @@ -94,7 +94,7 @@ func _register_event(): var exit_scene_event = \ escoria.esc_compiler.compile(exit_scene_script_builder.build(), get_class()) \ - .events[escoria.event_manager.EVENT_EXIT_SCENE] + .get_event_with_target(escoria.event_manager.EVENT_EXIT_SCENE) escoria.object_manager.get_object(self.global_id) \ - .events[escoria.event_manager.EVENT_EXIT_SCENE] = exit_scene_event + .events.add(exit_scene_event) diff --git a/addons/escoria-core/game/core-scripts/esc_game.gd b/addons/escoria-core/game/core-scripts/esc_game.gd index 9a424ad..ae155bb 100644 --- a/addons/escoria-core/game/core-scripts/esc_game.gd +++ b/addons/escoria-core/game/core-scripts/esc_game.gd @@ -1,8 +1,8 @@ ## Base class for ESC game scenes ## An extending class can be used in the project settings and is responsible ## for managing very basic game features and controls -extends Node2D class_name ESCGame +extends Node2D ## Signal emitted when the user has confirmed the crash popup[br] @@ -25,7 +25,7 @@ signal request_pause_menu ## Editor debug modes[br] ## NONE - No debugging[br] ## MOUSE_TOOLTIP_LIMITS - Visualize the tooltip limits -enum EDITOR_GAME_DEBUG_DISPLAY { +enum EditorGameDebugDisplay { NONE, # No debugging MOUSE_TOOLTIP_LIMITS # Visualize the tooltip limits } @@ -41,7 +41,7 @@ enum EDITOR_GAME_DEBUG_DISPLAY { @export var mouse_tooltip_margin: float = 50.0 ## Debug mode for the editor (None, Mouse Tooltips Limit) -@export var editor_debug_mode: EDITOR_GAME_DEBUG_DISPLAY = EDITOR_GAME_DEBUG_DISPLAY.NONE: +@export var editor_debug_mode: EditorGameDebugDisplay = EditorGameDebugDisplay.NONE: set = _set_editor_debug_mode ## The Control node underneath which all UI must be placed. @@ -143,10 +143,10 @@ func _ready(): func _draw(): if not Engine.is_editor_hint(): return - if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.NONE: + if editor_debug_mode == EditorGameDebugDisplay.NONE: return - if editor_debug_mode == EDITOR_GAME_DEBUG_DISPLAY.MOUSE_TOOLTIP_LIMITS: + if editor_debug_mode == EditorGameDebugDisplay.MOUSE_TOOLTIP_LIMITS: var mouse_limits: Rect2 = get_viewport_rect().grow( -mouse_tooltip_margin ) @@ -319,7 +319,7 @@ func left_double_click_on_bg(position: Vector2) -> void: ## #### Returns[br] ## [br] ## Returns nothing. -func element_focused(element_id: String) -> void: +func element_focused(_element_id: String) -> void: pass @@ -412,8 +412,8 @@ func left_double_click_on_item( ## [br] ## Returns nothing. func left_click_on_inventory_item( - inventory_item_global_id: String, - event: InputEvent + _inventory_item_global_id: String, + _event: InputEvent ) -> void: pass @@ -431,8 +431,8 @@ func left_click_on_inventory_item( ## [br] ## Returns nothing. func right_click_on_inventory_item( - inventory_item_global_id: String, - event: InputEvent + _inventory_item_global_id: String, + _event: InputEvent ) -> void: pass @@ -450,8 +450,8 @@ func right_click_on_inventory_item( ## [br] ## Returns nothing. func left_double_click_on_inventory_item( - inventory_item_global_id: String, - event: InputEvent + _inventory_item_global_id: String, + _event: InputEvent ) -> void: pass @@ -467,7 +467,7 @@ func left_double_click_on_inventory_item( ## #### Returns[br] ## [br] ## Returns nothing. -func inventory_item_focused(inventory_item_global_id: String) -> void: +func inventory_item_focused(_inventory_item_global_id: String) -> void: pass @@ -521,7 +521,7 @@ func close_inventory(): ## #### Returns[br] ## [br] ## Returns nothing. -func mousewheel_action(direction: int): +func mousewheel_action(_direction: int): pass @@ -557,7 +557,7 @@ func show_ui(): ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |p_editor_debug_mode|`int`|EDITOR_GAME_DEBUG_DISPLAY enum (int) value corresponding to the desired editor debug mode|yes|[br] +## |p_editor_debug_mode|`int`|EditorGameDebugDisplay enum (int) value corresponding to the desired editor debug mode|yes|[br] ## [br] ## #### Returns[br] ## [br] @@ -659,7 +659,7 @@ func hide_main_menu(): ## #### Returns[br] ## [br] ## Returns nothing. -func apply_custom_settings(custom_settings: Dictionary): +func apply_custom_settings(_custom_settings: Dictionary): pass diff --git a/addons/escoria-core/game/core-scripts/esc_interaction_location.gd b/addons/escoria-core/game/core-scripts/esc_interaction_location.gd index 4fe84fb..1f09e3d 100644 --- a/addons/escoria-core/game/core-scripts/esc_interaction_location.gd +++ b/addons/escoria-core/game/core-scripts/esc_interaction_location.gd @@ -5,8 +5,8 @@ ## automatically use an `ESCLocation` that is a child of the destination node.[br] ## Commands like `turn_to`--which are not movement-based--will ignore child ## `ESCLocation`s and refer to the parent node. -extends ESCLocation class_name ESCInteractionLocation +extends ESCLocation ## Whether this object's class is the same as given string class name.[br] ## [br] @@ -14,10 +14,10 @@ class_name ESCInteractionLocation ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |p_classname|`String`|String class to compare against|yes|[br] +## |p_classname|`String (4.6.x), StringName (4.7.x)`|Class name to compare against this location.|yes|[br] ## [br] ## #### Returns[br] ## [br] ## Returns a `bool` value. (`bool`) -func is_class(p_classname: String) -> bool: +func is_class(p_classname) -> bool: return p_classname == "ESCInteractionLocation" diff --git a/addons/escoria-core/game/core-scripts/esc_item.gd b/addons/escoria-core/game/core-scripts/esc_item.gd index 5f58b33..604addc 100644 --- a/addons/escoria-core/game/core-scripts/esc_item.gd +++ b/addons/escoria-core/game/core-scripts/esc_item.gd @@ -11,12 +11,8 @@ ## chooses to "use" the exit - for example, saying a goodbye, or running a ## cutscene. Place a ``change_scene`` command inside this event to move the ## character to the next room. -extends Area2D class_name ESCItem - - -## List of forbidden characters in global_ids -const FORBIDDEN_CHARACTERS: String = "['\"]" +extends Area2D ## Emitted when the mouse has entered this item[br] @@ -79,6 +75,26 @@ signal mouse_right_clicked_item(global_id) ## [br] signal arrived(walk_context) +## Emitted when the item started talking. +## [br] +signal started_talking + +## Emitted when the item stopped talking. +## [br] +signal stopped_talking + +## Emitted when the item started walking. +## [br] +signal started_walking + +## Emitted when the item stopped walking. +## [br] +signal stopped_walking + + +## List of forbidden characters in global_ids +const FORBIDDEN_CHARACTERS: String = "['\"]" + ## The global ID of this item @export var global_id: String @@ -108,10 +124,10 @@ signal arrived(walk_context) ## Default action to use if object is not in the inventory. @export var default_action: String -## If action used by player is in this list, the game will wait for a second -## click on another item to combine objects together (typically -## `USE <X> WITH <Y>`, `GIVE <X> TO <Y>`) -@export var combine_when_selected_action_is_in: PackedStringArray = [] +## Actions in this list make the item act as a source object and wait for a +## second clicked target object before running the action, typically +## `USE <X> WITH <Y>` or `GIVE <X> TO <Y>`. +@export var actions_requiring_target_object: PackedStringArray = [] ## If enabled, the ASHES script may have an :exit_scene event to manage scene changes. ## For simple exits that do not require scripted actions, the ESCExit node may be @@ -146,11 +162,9 @@ signal arrived(walk_context) ## If hover is enabled, applies this color modulation on the ESCItem sprite. @export var hover_modulate: Color = Color.WHITE -var _previous_color_modulate: Color ## If hover is enabled, replaces this ESCItem sprite texture by this one. @export var hover_texture: Texture2D = null -var _previous_texture: Texture2D = null ## If hover is enabled, applies this shader to the ESCItem sprite. @export var hover_shader: ShaderMaterial = null @@ -246,6 +260,12 @@ var _movable: ESCMovable = null ## The identified animation player var _animation_player: ESCAnimationPlayer = null +## Original sprite color modulation restored when hover ends. +var _previous_color_modulate: Color + +## Original sprite texture restored when hover ends. +var _previous_texture: Texture2D = null + ## Whether to force regsitration with the object manager. Defaults to false. var _force_registration: bool = false @@ -285,6 +305,7 @@ func _ready(): if not escoria.event_manager.event_finished.is_connected(_update_terrain): escoria.event_manager.event_finished.connect(_update_terrain) + _force_registration = true if escoria.save_manager.is_loading_game else false escoria.object_manager.register_object( ESCObject.new( global_id, @@ -523,7 +544,7 @@ func _unhandled_input(input_event: InputEvent) -> void: event.position = get_global_mouse_position() if event is InputEventMouseButton and event.is_pressed(): - if not escoria.current_state == escoria.GAME_STATE.DEFAULT: + if not escoria.current_state == escoria.GameState.DEFAULT: ESCSafeLogging.log_info( self, "Current game state doesn't accept interactions." @@ -550,10 +571,10 @@ func _unhandled_input(input_event: InputEvent) -> void: ## [br] ## #### Returns[br] ## [br] -## Returns a `String` value containing configuration warnings joined by newlines. (`String`) +## Returns a `PackedStringArray` value containing configuration warnings func _get_configuration_warnings(): validate_animations(animations) - return "\n".join(_scene_warnings) + return _scene_warnings ## Tests whether given point is inside a shape.[br] ## [br] @@ -650,8 +671,8 @@ func set_animations(p_animations: ESCAnimationResource) -> void: animations = p_animations - if not animations.is_connected("changed", Callable(self, "_validate_animations")): - animations.connect("changed", Callable(self, "_validate_animations")) + if not animations.changed.is_connected(validate_animations): + animations.changed.connect(validate_animations) ## The animation player node[br] @@ -859,6 +880,7 @@ func teleport_to(target: Vector2) -> void: ## Returns nothing. func walk_to(pos: Vector2, p_walk_context: ESCWalkContext = null) -> void: if is_movable: + started_walking.emit() _movable.walk_to(pos, p_walk_context) else: ESCSafeLogging.log_warn( @@ -883,6 +905,7 @@ func stop_walking_now(to_target: bool = false) -> void: var where: Vector2 = position if to_target: where = _movable.walk_destination + stopped_walking.emit() _movable.walk_stop(where) else: ESCSafeLogging.log_warn( @@ -931,11 +954,11 @@ func has_moved() -> bool: func has_sprite() -> bool: if _sprite_node != null: return true - else: # confirm - for child in self.get_children(): - if child is AnimatedSprite2D or child is Sprite2D: - return true - return false + + for child in self.get_children(): + if child is AnimatedSprite2D or child is Sprite2D: + return true + return false ## Return the sprite node.[br] ## [br] @@ -1093,6 +1116,7 @@ func start_talking(): animation_player.play( animations.speaks[animation_direction].animation ) + started_talking.emit(animation_direction) ## Stop playing the talking animation[br] @@ -1128,6 +1152,7 @@ func stop_talking(): animation_player.play( animations.idles[0].animation ) + stopped_talking.emit() ## Replay the last idle animation[br] @@ -1193,7 +1218,7 @@ func _detect_children() -> void: ## #### Returns[br] ## [br] ## Returns nothing. -func _update_terrain(rc: int, event_name: String) -> void: +func _update_terrain(_rc: int, event_name: String) -> void: if is_movable: _movable.update_terrain(event_name) @@ -1261,15 +1286,15 @@ func _get_inventory_texture() -> Texture2D: if c is TextureRect or c is Sprite2D: return c.texture return null - else: - return inventory_texture + + return inventory_texture func _get_inventory_texture_hovered() -> Texture2D: if inventory_texture_hovered == null: return _get_inventory_texture() - else: - return inventory_texture_hovered + + return inventory_texture_hovered ## Checks whether the given ESCAnimationResource property array has all non-null entries, and adds to the scene's warnings if not[br] ## [br] @@ -1307,8 +1332,8 @@ func _validate_animations_property_all_not_null(property: Array, property_name: func _get_identifier_as_key_value() -> String: if self.global_id: return "global_id: %s" % self.global_id - else: - return "node: %s" % get_name() + + return "node: %s" % get_name() ## Applies hover behavior on this ESCItem: if hover is enabled, applies the defined hover color modulate and/or hover texture and/or hover shader on the Sprite2D node, if any.[br] ## [br] diff --git a/addons/escoria-core/game/core-scripts/esc_location.gd b/addons/escoria-core/game/core-scripts/esc_location.gd index 47e9f09..8d8d93f 100644 --- a/addons/escoria-core/game/core-scripts/esc_location.gd +++ b/addons/escoria-core/game/core-scripts/esc_location.gd @@ -5,8 +5,8 @@ ## automatically use an `ESCLocation` that is a child of the destination node. ## Commands like `turn_to`--which are not movement-based--will ignore child ## `ESCLocation`s and refer to the parent node. -extends Marker2D class_name ESCLocation +extends Marker2D ## Escoria Plugin signal emitted to the `ESCRoom` when a start location is set in the `ESCLocation` node in order to check whether multiple start locations are set.[br] @@ -56,12 +56,12 @@ var _multiple_start_locations_exist: bool = false: ## [br] ## | Name | Type | Description | Required? |[br] ## |:-----|:-----|:------------|:----------|[br] -## |p_classname|`String`|Class name to compare against this location.|yes|[br] +## |p_classname|`String (4.6.x), StringName (4.7.x)`|Class name to compare against this location.|yes|[br] ## [br] ## #### Returns[br] ## [br] ## Returns a `bool` value. (`bool`) -func is_class(p_classname: String) -> bool: +func is_class(p_classname) -> bool: return p_classname == "ESCLocation" @@ -77,11 +77,16 @@ func is_class(p_classname: String) -> bool: func _ready(): if not Engine.is_editor_hint(): if not self.global_id.is_empty(): + var force_registration = false + if escoria.save_manager.is_loading_game: + force_registration = true escoria.object_manager.register_object( ESCObject.new( self.global_id, self - ) + ), + null, + force_registration ) ## Escoria editor plugin: on tree exit (ie. this node was removed), notify ESCRoom to update the list of start locations.[br] diff --git a/addons/escoria-core/game/core-scripts/esc_player.gd b/addons/escoria-core/game/core-scripts/esc_player.gd index 2ad2f15..5e7779d 100644 --- a/addons/escoria-core/game/core-scripts/esc_player.gd +++ b/addons/escoria-core/game/core-scripts/esc_player.gd @@ -1,8 +1,8 @@ @tool @icon("res://addons/escoria-core/design/esc_player.svg") ## An Escoria playable character. -extends ESCItem class_name ESCPlayer +extends ESCItem ## Whether the player can be hovered over and actions performed on like an item. diff --git a/addons/escoria-core/game/core-scripts/esc_resource_cache.gd b/addons/escoria-core/game/core-scripts/esc_resource_cache.gd index b096cca..ffc5ff1 100644 --- a/addons/escoria-core/game/core-scripts/esc_resource_cache.gd +++ b/addons/escoria-core/game/core-scripts/esc_resource_cache.gd @@ -1,6 +1,6 @@ ## A cache for resources -extends Node class_name ESCResourceCache +extends Node signal resource_loading_progress(path, progress) @@ -23,7 +23,7 @@ func queue_resource(path: String, p_in_front: bool = false, p_permanent: bool = if path in pending: return - elif ResourceLoader.has_cached(path): + if ResourceLoader.has_cached(path): var res = ResourceLoader.load(path) pending[path] = ESCResourceDescriptor.new(res, p_permanent) else: @@ -102,20 +102,19 @@ func get_resource(path): return res - else: - var res = pending[path].res - if !pending[path].permanent: - pending.erase(path) + var res = pending[path].res + if !pending[path].permanent: + pending.erase(path) - return res - else: - # We can't use ESCProjectSettingsManager here since this method - # can be called from escoria._init() - if not ProjectSettings.get_setting("escoria/platform/skip_cache"): - var res = ResourceLoader.load(path) - pending[path] = ESCResourceDescriptor.new(res, true) - return res - return ResourceLoader.load(path) + return res + + # We can't use ESCProjectSettingsManager here since this method + # can be called from escoria._init() + if not ProjectSettings.get_setting("escoria/platform/skip_cache"): + var res = ResourceLoader.load(path) + pending[path] = ESCResourceDescriptor.new(res, true) + return res + return ResourceLoader.load(path) func print_progress(p_path, p_progress): diff --git a/addons/escoria-core/game/core-scripts/esc_room.gd b/addons/escoria-core/game/core-scripts/esc_room.gd index d86780d..a69a7c4 100644 --- a/addons/escoria-core/game/core-scripts/esc_room.gd +++ b/addons/escoria-core/game/core-scripts/esc_room.gd @@ -1,8 +1,8 @@ @tool @icon("res://addons/escoria-core/design/esc_room.svg") ## A room in an Escoria based game. -extends Node2D class_name ESCRoom +extends Node2D ## Debugging displays for a room. @@ -168,8 +168,8 @@ func _connect_location_nodes() -> void: func _connect_location_nodes_in_tree(node: Node) -> void: for n in node.get_children(): if n is ESCLocation: - if not n.is_connected("editor_is_start_location_set",_validate_start_locations): - n.connect("editor_is_start_location_set", _validate_start_locations) + if not n.editor_is_start_location_set.is_connected(_validate_start_locations): + n.editor_is_start_location_set.connect(_validate_start_locations) if n.get_child_count() > 0: _connect_location_nodes_in_tree(n) diff --git a/addons/escoria-core/game/core-scripts/esc_terrain.gd b/addons/escoria-core/game/core-scripts/esc_terrain.gd index a9279f8..d899670 100644 --- a/addons/escoria-core/game/core-scripts/esc_terrain.gd +++ b/addons/escoria-core/game/core-scripts/esc_terrain.gd @@ -1,12 +1,8 @@ @tool @icon("res://addons/escoria-core/design/esc_terrain.svg") ## A walkable Terrain for Escoria rooms. -extends Node2D class_name ESCTerrain - - -## Logger class reference -const EscLogger = preload("res://addons/escoria-core/tools/logging/esc_logger.gd") +extends Node2D ## Visualize scales or lightmap for debugging purposes if the editor @@ -17,6 +13,9 @@ enum DebugMode { } +## Logger class reference +const EscLogger = preload("res://addons/escoria-core/tools/logging/esc_logger.gd") + @export_group("Scales") ## Scaling texture. If empty, items scale will always be 1.0. @@ -169,9 +168,8 @@ func _check_multiple_enabled_navpolys(node: Node = null, is_exiting: bool = fals "at the same time." ) return - else: - navigation_enabled_found = true - current_active_navigation_instance = n + navigation_enabled_found = true + current_active_navigation_instance = n ## Return the Color of the lightmap pixel for the specified position.[br] diff --git a/addons/escoria-core/game/core-scripts/esc_tooltip.gd b/addons/escoria-core/game/core-scripts/esc_tooltip.gd index 937856a..52ddc1e 100644 --- a/addons/escoria-core/game/core-scripts/esc_tooltip.gd +++ b/addons/escoria-core/game/core-scripts/esc_tooltip.gd @@ -1,7 +1,7 @@ @tool ## Dynamically controlled tooltip -extends RichTextLabel class_name ESCTooltip +extends RichTextLabel ## Maximum width of the label @@ -162,7 +162,6 @@ func update_tooltip_text(): """ Overriden method. Should not be called directly. """ - pass ## Update the tooltip size according to the text.[br] diff --git a/addons/escoria-core/game/core-scripts/esc_walk_context.gd b/addons/escoria-core/game/core-scripts/esc_walk_context.gd index c0b7c86..e0aa5a6 100644 --- a/addons/escoria-core/game/core-scripts/esc_walk_context.gd +++ b/addons/escoria-core/game/core-scripts/esc_walk_context.gd @@ -1,7 +1,7 @@ ## The walk context describes the target of a walk command and if that command ## should be executed fast. -extends RefCounted class_name ESCWalkContext +extends RefCounted ## Target object that the walk command tries to reach. diff --git a/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd b/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd index 1592f27..7aecd73 100644 --- a/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd +++ b/addons/escoria-core/game/core-scripts/migrations/esc_migration.gd @@ -1,5 +1,5 @@ -extends RefCounted class_name ESCMigration +extends RefCounted ## Base class for all migration version scripts. Extending scripts should be ## named like the version they migrate the savegame to. (e.g. 1.0.0.gd, 1.0.1.gd) diff --git a/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd b/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd index 8db1a0d..53a833a 100644 --- a/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd +++ b/addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd @@ -1,7 +1,7 @@ ## Class that handles migrations between different game or escoria versions ## @MANAGER -extends RefCounted class_name ESCMigrationManager +extends RefCounted ## Regular expression that matches a simple semver version string @@ -160,18 +160,22 @@ func _version_between(version: String, from: String, to: String) -> bool: if from_info.get_string("major") < version_info.get_string("major") and \ version_info.get_string("major") < to_info.get_string("major"): return true - elif from_info.get_string("major") == version_info.get_string("major") and \ + + if from_info.get_string("major") == version_info.get_string("major") and \ from_info.get_string("minor") < version_info.get_string("minor"): return true - elif from_info.get_string("major") == version_info.get_string("major") and \ + + if from_info.get_string("major") == version_info.get_string("major") and \ from_info.get_string("minor") == \ version_info.get_string("minor") and \ from_info.get_string("patch") < version_info.get_string("patch"): return true - elif to_info.get_string("major") == version_info.get_string("major") and \ + + if to_info.get_string("major") == version_info.get_string("major") and \ to_info.get_string("minor") > version_info.get_string("minor"): return true - elif to_info.get_string("major") == version_info.get_string("major") and \ + + if to_info.get_string("major") == version_info.get_string("major") and \ to_info.get_string("minor") == version_info.get_string("minor") and\ to_info.get_string("patch") > version_info.get_string("patch"): return true @@ -197,10 +201,12 @@ func _compare_version(version_a: String, version_b: String) -> bool: if a_info.get_string("major") < b_info.get_string("major"): return true - elif a_info.get_string("major") == b_info.get_string("major") and \ + + if a_info.get_string("major") == b_info.get_string("major") and \ a_info.get_string("minor") < b_info.get_string("minor"): return true - elif a_info.get_string("major") == b_info.get_string("major") and \ + + if a_info.get_string("major") == b_info.get_string("major") and \ a_info.get_string("minor") == b_info.get_string("minor") and \ a_info.get_string("patch") < b_info.get_string("patch"): return true diff --git a/addons/escoria-core/game/core-scripts/pre-4.7/.gdignore b/addons/escoria-core/game/core-scripts/pre-4.7/.gdignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/pre-4.7/.gdignore diff --git a/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd b/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd new file mode 100644 index 0000000..678fad0 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd @@ -0,0 +1,1434 @@ +# 4.6 +@tool +@icon("res://addons/escoria-core/design/esc_item.svg") +## An ``ESCItem`` defines a (usually interactive) item in the game. +## +## When interacting with an ``ESCItem``, the game character will automatically +## walk to an ``ESCLocation`` that is created as a child of an ``ESCItem``.[br] +##[br] +## By selecting the "Is Exit" checkbox when you create an ``ESCItem`` +## node, Escoria will look for an ``:exit_scene`` event in the attached script file. +## Any commands you place in the ``:exit_scene`` event will be run when the player +## chooses to "use" the exit - for example, saying a goodbye, or running a +## cutscene. Place a ``change_scene`` command inside this event to move the +## character to the next room. +class_name ESCItem +extends Area2D + + +## Emitted when the mouse has entered this item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |item|`Variant`|Item that emitted the signal.|yes|[br] +## [br] +signal mouse_entered_item(item) + +## Emitted when the mouse has exited this item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |item|`Variant`|Item that emitted the signal.|yes|[br] +## [br] +signal mouse_exited_item(item) + +## Emitted when the item was left clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_left_clicked_item(global_id) + +## Emitted when the item was double clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_double_left_clicked_item(global_id) + +## Emitted when the item was right clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_right_clicked_item(global_id) + +## Emitted when the item walked to a destination[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |walk_context|`Variant`|The walk context of the command|yes|[br] +## [br] +signal arrived(walk_context) + +## Emitted when the item started talking. +## [br] +signal started_talking + +## Emitted when the item stopped talking. +## [br] +signal stopped_talking + +## Emitted when the item started walking. +## [br] +signal started_walking + +## Emitted when the item stopped walking. +## [br] +signal stopped_walking + + +## List of forbidden characters in global_ids +const FORBIDDEN_CHARACTERS: String = "['\"]" + + +## The global ID of this item +@export var global_id: String + +## The ASHES script for this item +@export_file("*.esc", "*.ash") var esc_script: String + +## The node that references the camera position and zoom if this item is used +## as a camera target +@export var camera_node: NodePath + +@export_group("Display") + +## The name for the tooltip of this item. +@export var tooltip_name: String + +@export_group("","") +@export_group("Item Behavior") + +## Whether this item is movable. A movable item will be scaled with the terrain +## and be moved with commands like ``teleport`` and ``turn_to``. +@export var is_movable: bool = false + +## Color used for dialogs if that item talks. +@export var dialog_color: Color = Color(1,1,1,1) + +## Default action to use if object is not in the inventory. +@export var default_action: String + +## Actions in this list make the item act as a source object and wait for a +## second clicked target object before running the action, typically +## `USE <X> WITH <Y>` or `GIVE <X> TO <Y>`. +@export var actions_requiring_target_object: PackedStringArray = [] + +## If enabled, the ASHES script may have an :exit_scene event to manage scene changes. +## For simple exits that do not require scripted actions, the ESCExit node may be +## preferred. +@export var is_exit: bool + +## Defines this item as acting as a trigger if enabled.[br] +## Allows using specific events (defined in trigger_in_verb and trigger_out_verb +## properties) in ASHES scripts. +@export var is_trigger: bool + +## Event name that is activated when another item enters the area defined by the +## trigger item. By default, "trigger_in". +@export var trigger_in_verb: String = "trigger_in" + +## Event name that is activated when another item exits the area defined by the +## trigger item. By default, "trigger_out". +@export var trigger_out_verb: String = "trigger_out" + +## Defines whether the player can interact with this item. If false, the item +## will not react to inputs and mouse hovers. +@export var is_interactive: bool = true + + +@export_subgroup("Hover Behavior") + +## Defines whether Escoria will manage a specific hover behavior when the item +## is focused. All options below can be used together.[br] +## This can also be expanded or overriden in your ESCGame implementation +## (in methods ``element_focused()`` and ``element_unfocused()``). +@export var hover_enabled: bool = false + +## If hover is enabled, applies this color modulation on the ESCItem sprite. +@export var hover_modulate: Color = Color.WHITE + +## If hover is enabled, replaces this ESCItem sprite texture by this one. +@export var hover_texture: Texture2D = null + +## If hover is enabled, applies this shader to the ESCItem sprite. +@export var hover_shader: ShaderMaterial = null + +@export_subgroup("","") + +@export_group("","") +@export_group("Player behavior on arrival") + +## Whether player character orients towards 'interaction_angle' as it arrives at +## the item's interaction position. +@export var player_orients_on_arrival: bool = true + +## If 'player_orients_on_arrival' is enabled, let the player character turn to +## this angle when it arrives at the item's interaction position. +@export var interaction_angle: int + +@export_group("","") +@export_group("Inventory") + +## Default action to use if object is in the inventory +@export var default_action_inventory: String + +## If enabled, combination must be done in the way it is written in ASHES script +## ie. :use ON_ITEM[br] +## If disabled, combination will be tried in the other way. +@export var combine_is_one_way: bool = false + +## If enabled, then the object must have been picked up before using it. +## Keep disabled for items in the background, such as buttons. +@export var use_from_inventory_only: bool = false + +## The visual representation for this item when it's in the inventory +@export var inventory_texture: Texture2D = null: + get = _get_inventory_texture + +## The visual representation for this item when it's in the inventory and hovered +@export var inventory_texture_hovered: Texture2D = null: + get = _get_inventory_texture_hovered + +@export_group("","") +@export_group("Movement") + +## If enabled, terrain scaling will not be applied and +## node will remain at the scale set in the scene. +@export var dont_apply_terrain_scaling: bool = false + +## Speed of this item if movable +@export var speed: int = 300 + +## Speed damp of this item if movable +@export var v_speed_damp: float = 1.0 + +@export_group("","") +@export_group("Animations") + +##Â Animations resource for the item (walking, idling...) +@export var animations: ESCAnimationResource: + set = set_animations + +## The node used to play animations +@export var animation_player_node: NodePath = "": + set = _set_animation_player_node + +@export_group("","") +@export_group("Custom actions") + +## Custom data dictionary to ease customization and custom command creation. +## Avoid name collision using proper key names. +@export var custom_data: Dictionary = {}: + get = get_custom_data, + set = set_custom_data + + +@export_group("","") + +## Reference to the animation node (null if none was found) +var animation_sprite = null + +## Reference to the current terrain +var terrain: ESCTerrain + +## Reference to this items collision shape node +var collision: Node + + +## Reference to the sprite node +var _sprite_node: Node = null + +## The movable subnode +var _movable: ESCMovable = null + +## The identified animation player +var _animation_player: ESCAnimationPlayer = null + +## Original sprite color modulation restored when hover ends. +var _previous_color_modulate: Color + +## Original sprite texture restored when hover ends. +var _previous_texture: Texture2D = null + +## Whether to force regsitration with the object manager. Defaults to false. +var _force_registration: bool = false + +## Warnings for scene. +var _scene_warnings: PackedStringArray = [] + + +# Add the movable node, connect signals, detect child nodes +# and register this item +func _ready(): + self.process_mode = Node.PROCESS_MODE_PAUSABLE + + _detect_children() + + # We add ourselves to this group so we can easily get a reference to all + # items in a scene tree. + if not Engine.is_editor_hint(): + add_to_group(escoria.GROUP_ITEM_CAN_COLLIDE) + if is_trigger: + add_to_group(escoria.GROUP_ITEM_TRIGGERS) + + validate_animations(animations) + validate_exported_parameters() + + if not input_event.is_connected(_on_input_event): + input_event.connect(_on_input_event) + if not mouse_exited.is_connected(_on_mouse_exited): + mouse_exited.connect(_on_mouse_exited) + + # Register and connect all elements to Escoria backoffice. + if not Engine.is_editor_hint(): + + if is_movable: + _movable = ESCMovable.new() + add_child(_movable) + + if not escoria.event_manager.event_finished.is_connected(_update_terrain): + escoria.event_manager.event_finished.connect(_update_terrain) + + _force_registration = true if escoria.save_manager.is_loading_game else false + escoria.object_manager.register_object( + ESCObject.new( + global_id, + self + ), + null, + _force_registration + ) + + terrain = escoria.room_terrain if is_instance_valid(escoria.room_terrain) else null + + if escoria.object_manager.get_object(global_id).state == ESCObject.STATE_DEFAULT \ + and get_animation_player() != null: + escoria.object_manager.get_object(global_id) \ + .set_state(get_animation_player().get_animation()) + if is_movable: + escoria.object_manager.get_object(global_id).node \ + ._movable.last_dir = animations.get_direction_id_from_animation_name( + get_animation_player().get_animation() + ) + + + if escoria.object_manager.get_object(global_id).state == ESCObject.STATE_DEFAULT \ + and escoria.object_manager.get_object(global_id).node.get_animation_player() != null: + escoria.object_manager.get_object(global_id) \ + .set_state(get_animation_player().get_animation()) + if is_movable: + escoria.object_manager.get_object(global_id).node._movable.last_dir = 1 + #animations.get_direction_id_from_animation_name( + #.get_animation_player().get_animation() + #) + + if !is_trigger: + if not self.is_connected( + "mouse_entered_item", + escoria.inputs_manager._on_mouse_entered_item + ): + mouse_entered_item.connect( + escoria.inputs_manager._on_mouse_entered_item + ) + if not self.is_connected( + "mouse_exited_item", + escoria.inputs_manager._on_mouse_exited_item + ): + mouse_exited_item.connect( + escoria.inputs_manager._on_mouse_exited_item + ) + if not self.is_connected( + "mouse_left_clicked_item", + escoria.inputs_manager._on_mouse_left_clicked_item + ): + mouse_left_clicked_item.connect( + escoria.inputs_manager._on_mouse_left_clicked_item + ) + if not self.is_connected( + "mouse_double_left_clicked_item", + escoria.inputs_manager._on_mouse_left_double_clicked_item + ): + mouse_double_left_clicked_item.connect( + escoria.inputs_manager._on_mouse_left_double_clicked_item + ) + if not self.is_connected( + "mouse_right_clicked_item", + escoria.inputs_manager._on_mouse_right_clicked_item + ): + mouse_right_clicked_item.connect( + escoria.inputs_manager._on_mouse_right_clicked_item + ) + else: # Item is a trigger + if not self.is_connected("area_entered", _element_entered): + area_entered.connect(_element_entered) + if not self.is_connected("area_exited", _element_exited): + area_exited.connect(_element_exited) + if not self.is_connected("body_entered", _element_entered): + body_entered.connect(_element_entered) + if not self.is_connected("body_exited", _element_exited): + body_exited.connect(_element_exited) + + # If object can be in the inventory, set default_action_inventory to same as + # default_action, if default_action_inventory is not set + if use_from_inventory_only and default_action_inventory.is_empty(): + default_action_inventory = default_action + + # Perform a first terrain scaling if we have to. + if (not is_exit or dont_apply_terrain_scaling) and is_movable: + _movable.last_scale = scale + _movable.update_terrain() + +## For trigger items, enable monitoring to detect collisions.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func connect_trigger_events(): + assert(is_trigger) + self.monitoring = true + + +## Validates the various exported parameters so we get immediate crash.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func validate_exported_parameters() -> void: + var regex = RegEx.new() + regex.compile(FORBIDDEN_CHARACTERS) + var result = regex.search(global_id) + if result: + ESCSafeLogging.log_error( + self, + "Forbidden character in global_id %s (path: %s)" + % [global_id, get_path()] + ) + if global_id.is_empty(): + ESCSafeLogging.log_error( + self, + "global_id of item is empty (node name : %s, path: %s)" + % [name, get_path()] + ) + +## For trigger items, disable monitoring to disable collisions detection.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func disconnect_trigger_events(): + assert(is_trigger) + self.monitoring = false + + +## Mouse exited happens on any item that mouse cursor exited, even those UNDER the top level of overlapping stack.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _on_mouse_exited(): + if escoria.inputs_manager.hover_stack.has(self): + escoria.inputs_manager.hover_stack.erase_item(self) + escoria.inputs_manager.unset_hovered_node(self) + _apply_unhover_behavior() + +## Special helper class used to sort stacked items by z-index. +class HoverStackSorter: + static func sort_ascending_z_index(a, b): + if a.z_index < b.z_index: + return true + return false + + +## Manage input events on the item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |_viewport|`Object`|the viewport node the event entered|yes|[br] +## |event|`InputEvent`|Input event delivered to the item.|yes|[br] +## |_shape_idx|`int`|Index of the collision shape within the item that received the event.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _on_input_event(_viewport: Object, event: InputEvent, _shape_idx: int): + if event is InputEventMouseMotion: + var physics2d_dss: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state + var params := PhysicsPointQueryParameters2D.new() + params.position = get_global_mouse_position() + params.collision_mask = 0x7FFFFFFF + params.collide_with_areas = true + var colliding: Array = physics2d_dss.intersect_point(params, 32) + var colliding_nodes = [] + for c in colliding: + if c.collider.get("global_id") \ + and escoria.action_manager.is_object_actionable(c.collider.global_id): + colliding_nodes.push_back(c.collider) + + if colliding_nodes.is_empty(): + return + colliding_nodes.sort_custom(Callable(HoverStackSorter, "sort_ascending_z_index")) + escoria.inputs_manager.hover_stack.clear() + escoria.inputs_manager.hover_stack.add_items(colliding_nodes) + escoria.inputs_manager.set_hovered_node(colliding_nodes.back()) + +## Manage mouse button clicks on this item by sending out signals[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |input_event|`InputEvent`|Triggered event|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _unhandled_input(input_event: InputEvent) -> void: + # If this is a trigger, then escoria.inputs_manager is not wired up to + # receive the signals this function might dispatch. In particular, + # calling get_tree().set_input_as_handled() unnecessarily will prevent + # the ESCBackground from being able to process the event. + # See https://github.com/godot-escoria/escoria-issues/issues/147. + if is_trigger: + return + + var event = input_event + # Note that event could be InputEventMouseButton, InputEventJoypadButton, + # or something else. As such, the value of the `button_index` property + # must be read in the context of the type of input event. + if input_event is InputEventJoypadButton: + if not input_event.is_action_pressed(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION): + return + + # For now, rather than refactor input handling to be more generic + # to accommodate gamepad support, we create a synthetic mouse event + # based on the InputEventJoypadButton. + event = InputEventMouseButton.new() + event.button_index = MOUSE_BUTTON_LEFT + event.double_click = false + event.button_pressed = true + # ESCActionManager expects to read the position off of the event. + event.position = get_global_mouse_position() + + if event is InputEventMouseButton and event.is_pressed(): + if not escoria.current_state == escoria.GameState.DEFAULT: + ESCSafeLogging.log_info( + self, + "Current game state doesn't accept interactions." + ) + return + var p = get_global_mouse_position() + if _is_in_shape(p) and escoria.action_manager.is_object_actionable(global_id): + if event.double_click and event.button_index == MOUSE_BUTTON_LEFT: + mouse_double_left_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + elif event.button_index == MOUSE_BUTTON_LEFT: + mouse_left_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + elif event.button_index == MOUSE_BUTTON_RIGHT: + mouse_right_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + + +## To display warnings in the scene tree should there be any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `String` value containing configuration warnings joined by newlines. (`String`) +func _get_configuration_warnings(): + validate_animations(animations) + return "\n".join(_scene_warnings) + + +## Tests whether given point is inside a shape.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |position|`Vector2`|point coordinates to test|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func _is_in_shape(position: Vector2) -> bool: + var params := PhysicsPointQueryParameters2D.new() + params.position = position + params.collision_mask = 2147483647 + params.collide_with_areas = true + var colliders = get_world_2d().direct_space_state.intersect_point( + params, + 32 + ) + for _owner in get_shape_owners(): + for _shape_id in range(0, shape_owner_get_shape_count(_owner)): + for _collider in colliders: + if _collider.collider == self and _collider.shape == _shape_id: + return true + return false + + +## Validates the ESCAnimationResource if it exists. Note that we pass in the ESCAnimationResource as an argument so that it can also be used to validate an ESCAnimationResource prior to being set.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |animations_resource|`ESCAnimationResource`|Animation resource to validate before using it on this item.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func validate_animations(animations_resource: ESCAnimationResource) -> void: + if not is_instance_valid(animations_resource): + return + + # This initialization must always be here since this is a tool script. + _scene_warnings = [] + + if is_instance_valid(animations_resource): + _validate_animations_property_all_not_null(animations_resource.dir_angles, "dir_angles") + + var num_dir_angles = animations_resource.dir_angles.size() + + if animations_resource.directions.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'directions' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.directions.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.directions, "directions") + + if animations_resource.idles.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'idles' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.idles.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.idles, "idles") + + if animations_resource.speaks.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'speaks' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.speaks.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.speaks, "speaks") + + if Engine.is_editor_hint(): + update_configuration_warnings() + elif _scene_warnings.size() > 0: + ESCSafeLogging.log_error( + self, + ", ".join(_scene_warnings) + ) + + +## Setter for the animations property.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |p_animations|`ESCAnimationResource`|the ESCAnimationResource to set.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_animations(p_animations: ESCAnimationResource) -> void: + if p_animations == null: + return + + animations = p_animations + + if not animations.changed.is_connected(validate_animations): + animations.changed.connect(validate_animations) + + +## The animation player node[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the animation player node. (`Node`) +func get_animation_player() -> Node: + if _animation_player == null: + var player_node_path = animation_player_node + if player_node_path.is_empty(): + for child in self.get_children(): + if child is AnimatedSprite2D or \ + child is AnimationPlayer: + player_node_path = child.get_path() + if player_node_path.is_empty(): + ESCSafeLogging.log_debug( + self, + "Can not find animation_player or animated sprite for %s." % global_id + ) + elif not has_node(player_node_path): + ESCSafeLogging.log_debug( + self, + "Can not find animation_player node at path %s for %s." % [player_node_path, global_id] + ) + else: + _animation_player = ESCAnimationPlayer.new( + get_node(player_node_path) + ) + return _animation_player + + +## Return the position the player needs to walk to to interact with this item. That can either be a direct Position2D child or a collision shape[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Vector2` value. (`Vector2`) +func get_interact_position() -> Vector2: + var pos_2d_count: int = 0 + var pos_2d_position = null + + var esclocation_count = 0 + var esclocation_position = null + + var interact_count = 0 + var interact_position = null + + for c in get_children(): + if c is Marker2D: + # Identify any Postion2D nodes + if c is ESCLocation: + esclocation_count += 1 + esclocation_position = c.global_position + elif c is ESCInteractionLocation: + interact_count += 1 + interact_position = c.global_position + else: + # This will catch all other Position2D related nodes + # including dialog locations and native Position2D nodes. + pos_2d_count += 1 + pos_2d_position = c.global_position + + if interact_position == null and \ + esclocation_position == null and is_instance_valid(collision): + ESCSafeLogging.log_warn( + self, + "No ESCLocation found to walk to for object " + + "%s. Middle of collision shape will be used." % global_id) + return collision.global_position + + if interact_count > 0: + if interact_count > 1: + ESCSafeLogging.log_warn( + self, + "Multiple ESCInteractionLocations found to walk to for " + + "object %s. Last one will be used." % global_id) + return interact_position + + if esclocation_count > 1: + ESCSafeLogging.log_warn( + self, + "Multiple ESClocations found to walk to for object " + + "%s. Last one will be used." % global_id) + return esclocation_position + + +## React to the mouse entering the item by emitting the respective signal[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func mouse_entered(): + if escoria.action_manager.is_object_actionable(global_id): + mouse_entered_item.emit(self) + _apply_hover_behavior() + + + +## React to the mouse exiting the item by emitting the respective signal[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func do_mouse_exited(): + mouse_exited_item.emit(self) + + +# Another item (e.g. the player) has entered this item +# +# #### Parameters +# +# - body: Other object that has entered the item +func _element_entered(body): + if body is ESCBackground or body.get_parent() is ESCBackground: + return + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_IN, + [global_id, body.global_id, trigger_in_verb] + ) + + +# Another item (e.g. the player) has exited this element +# +# #### Parameters +# +# - body: Other object that has exited the item +func _element_exited(body): + if body is ESCBackground or body.get_parent() is ESCBackground: + return + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_OUT, + [global_id, body.global_id, trigger_out_verb] + ) + + +## Use the movable node to teleport this item to the target item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |target|`Node`|Target node to teleport to|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func teleport(target: Node) -> void: + if is_movable: + _movable.teleport(target) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot \"teleport\". Its \"is_movable\" parameter is false." %self + ) + + +## Use the movable node to teleport this item to the target position[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |target|`Vector2`|Vector2 position to teleport to|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func teleport_to(target: Vector2) -> void: + if is_movable: + _movable.teleport_to(target) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot \"teleport_to\". Its \"is_movable\" parameter is false." %self + ) + + +## Use the movable node to make the item walk to the given position[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |pos|`Vector2`|Position to walk to|yes|[br] +## |p_walk_context|`ESCWalkContext`|Walk context to use|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func walk_to(pos: Vector2, p_walk_context: ESCWalkContext = null) -> void: + if is_movable: + started_walking.emit() + _movable.walk_to(pos, p_walk_context) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"walk_to\". Its \"is_movable\" parameter is false." %self + ) + + +## Stop the movable node immediately and remain where it is at this moment, or teleport it directly at destination position if 'to_target' is true.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |to_target|`bool`|if true, the movable node is teleport directly at its target destination|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func stop_walking_now(to_target: bool = false) -> void: + if is_movable: + var where: Vector2 = position + if to_target: + where = _movable.walk_destination + stopped_walking.emit() + _movable.walk_stop(where) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"stop_walking_now\". Its \"is_movable\" parameter is false." %self + ) + + +## Set the moving speed[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |speed_value|`int`|Set the new speed|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_velocity(speed_value: int) -> void: + speed = speed_value + + +## Checks whether this item moved[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func has_moved() -> bool: + return _movable.moved if is_movable else false + + +## Checks whether this item has a sprite.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func has_sprite() -> bool: + if _sprite_node != null: + return true + + for child in self.get_children(): + if child is AnimatedSprite2D or child is Sprite2D: + return true + return false + +## Return the sprite node.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Node` value. (`Node`) +func get_sprite() -> Node: + if _sprite_node == null: + for child in self.get_children(): + if child is AnimatedSprite2D or child is Sprite2D: + _sprite_node = child + if _sprite_node == null: + ESCSafeLogging.log_error( + self, + "No sprite node found in the scene %s." % get_path() + ) + return _sprite_node + + +## Set the angle[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |deg|`int`|The angle degree to set|yes|[br] +## |wait|`float`|Wait this amount of seconds until continuing with turning around|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_angle(deg: int, wait: float = 0.0): + if is_movable: + _movable.set_angle(deg, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"set_angle\". Its \"is_movable\" parameter is false." % self + ) + + +## Set the direction id[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |direction_id|`int`|The direction id|yes|[br] +## |wait|`float`|Wait this amount of seconds until continuing with turning around|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_direction(direction_id: int, wait: float = 0.0): + if is_movable: + _movable.set_direction(direction_id, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"set_direction\". Its \"is_movable\" parameter is false." % self + ) + + +## Turn to face another object[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |object|`Node`|Node whose position the item should face.|yes|[br] +## |wait|`float`|Optional delay in seconds before execution continues after turning.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func turn_to(object: Node, wait: float = 0.0): + if is_movable: + _movable.turn_to(object, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"turn_to\". Its \"is_movable\" parameter is false." % self + ) + + +## Check everything is in place to play talk animations. A warning indicating the problem is printed in console, if necessary.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func check_talk_possible() -> bool: + if is_movable and (_movable.last_dir < 0 \ + or _movable.last_dir >= animations.speaks.size()): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"last_dir\" parameter is invalid: %s." \ + % [self, _movable.last_dir] + ) + return false + if not is_instance_valid(animations): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"animations\" parameter is empty." \ + % self + ) + return false + if animations.speaks.size() == 0: + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"animations.speaks\" array is empty." \ + % self + ) + return false + if not get_animation_player(): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its animation player can't be found." \ + % self + ) + return false + return true + + +## Play the talking animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func start_talking(): + if not check_talk_possible(): + return + + var animation_player = get_animation_player() + + if animation_player.is_playing(): + animation_player.stop() + + if is_movable and animations.speaks[_movable.last_dir].mirrored \ + and not _movable.is_mirrored: + _sprite_node.scale.x *= -1 + + var animation_direction = _movable.last_dir if is_movable else 0 + animation_player.play( + animations.speaks[animation_direction].animation + ) + started_talking.emit(animation_direction) + + +## Stop playing the talking animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func stop_talking(): + if not check_talk_possible(): + return + + var animation_player = get_animation_player() + + if animation_player.is_playing(): + animation_player.stop() + + if is_movable: + if animations.speaks[_movable.last_dir].mirrored \ + and not _movable.is_mirrored: + # Allow this function to be called multiple times without setting + # the direction incorrectly + if _sprite_node.scale.x < 1: + _sprite_node.scale.x *= -1 + + animation_player.play( + animations.idles[_movable.last_dir].animation + ) + else: + animation_player.play( + animations.idles[0].animation + ) + stopped_talking.emit() + + +## Replay the last idle animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func update_idle(): + get_animation_player().play( + animations.idles[_movable.last_dir].animation + ) + + +## Return the camera position if a camera_position_node exists or the global position of the player[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Node` value. (`Node`) +func get_camera_node() -> Node: + if has_node(camera_node): + ESCSafeLogging.log_debug( + self, + "Camera3D node found - directing camera to the camera_node on %s." + % global_id + ) + return get_node(camera_node) + return self + + +## Detect the child nodes and set respective references.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _detect_children() -> void: + # Initialize collision variable. + for c in get_children(): + if c is CollisionShape2D or c is CollisionPolygon2D: + collision = c + + +## Update the terrain when an event finished[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |rc|`int`|Return code of the event that just finished|yes|[br] +## |event_name|`String`|Event that just finished to provide to update terrain call.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _update_terrain(_rc: int, event_name: String) -> void: + if is_movable: + _movable.update_terrain(event_name) + +## Godot method override to provide more information about this object.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _get_property_list(): + var properties = [] + properties.append({ + "name": "animations", + "type": TYPE_OBJECT, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + "hint_string": "ESCAnimationResource" + }) + return properties + + +## Set the node path to the animation player[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |node_path|`NodePath`|Path to the player node|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _set_animation_player_node(node_path: NodePath): + if not Engine.is_editor_hint(): + return + + if node_path.is_empty(): + animation_player_node = node_path + return + + assert(has_node(node_path), "Node with path %s not found" % node_path) + assert( + get_node(node_path) is AnimatedSprite2D or \ + get_node(node_path) is AnimationPlayer, + "Selected node has to be an AnimatedSprite2D or AnimationPlayer node" + ) + + animation_player_node = node_path + + +## Either the set inventory texture or the texture of a TextureRect found as a child if it is null[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns either the set inventory texture or the texture of a TextureRect found as a child if it is null. (`Texture2D`) +func _get_inventory_texture() -> Texture2D: + if inventory_texture == null: + for c in get_children(): + if c is TextureRect or c is Sprite2D: + return c.texture + return null + + return inventory_texture + + +func _get_inventory_texture_hovered() -> Texture2D: + if inventory_texture_hovered == null: + return _get_inventory_texture() + + return inventory_texture_hovered + +## Checks whether the given ESCAnimationResource property array has all non-null entries, and adds to the scene's warnings if not[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |property|`Array`|ESCAnimationResource property. Must be an array.|yes|[br] +## |property_name|`String`|the name of the property being passed in.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _validate_animations_property_all_not_null(property: Array, property_name: String) -> void: + var has_empty_entry: bool = false + + for item in property: + if item == null: + has_empty_entry = true + break + + if has_empty_entry: + _scene_warnings.append("At least one entry in '%s' is empty. [%s]" % [property_name, _get_identifier_as_key_value()]) + + +## The global ID as a key/value pair. If none is specified, use the node name. Used to tag messages.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the global ID as a key/value pair. If none is specified, use the node name. Used to tag messages. (`String`) +func _get_identifier_as_key_value() -> String: + if self.global_id: + return "global_id: %s" % self.global_id + + return "node: %s" % get_name() + +## Applies hover behavior on this ESCItem: if hover is enabled, applies the defined hover color modulate and/or hover texture and/or hover shader on the Sprite2D node, if any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _apply_hover_behavior() -> void: + if not hover_enabled: + return + if has_sprite(): + var sprite = get_sprite() + _previous_color_modulate = modulate + sprite.modulate = hover_modulate + if sprite is Sprite2D: + if hover_texture != null: + _previous_texture = sprite.texture + sprite.texture = hover_texture + if hover_shader != null: + sprite.material = hover_shader + +## Applies unhover behavior on this ESCItem: if hover is enabled, reverts back to normal the defined hover color modulate and/or hover texture and/or hover shader on the Sprite2D node, if any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _apply_unhover_behavior() -> void: + if not hover_enabled: + return + if has_sprite(): + var sprite = get_sprite() + sprite.modulate = _previous_color_modulate + if sprite is Sprite2D: + if hover_texture != null: + sprite.texture = _previous_texture + if hover_shader != null: + sprite.material = null + + +## Whether the item is currently moving.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns true if the player is currently moving, false otherwise. (`bool`) +func is_moving() -> bool: + return _movable.task != ESCMovable.MovableTask.NONE if is_movable else false + +## The number of defined animation directions for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the number of defined animation directions for this ESCItem. (`int`) +func get_directions_quantity() -> int: + return animations.dir_angles.size() + + +## A Dictionary containing custom data defined for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a Dictionary containing custom data defined for this ESCItem. (`Dictionary`) +func get_custom_data() -> Dictionary: + return custom_data + +## Sets the `custom_data` Dictionary containing custom data defined for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |data|`Dictionary`|Custom data dictionary to assign to this item (an empty dictionary is used when `null`).|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_custom_data(data: Dictionary) -> void: + custom_data = data if (data != null) else {} diff --git a/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd b/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd new file mode 100644 index 0000000..604addc --- /dev/null +++ b/addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd @@ -0,0 +1,1432 @@ +@tool +@icon("res://addons/escoria-core/design/esc_item.svg") +## An ``ESCItem`` defines a (usually interactive) item in the game. +## +## When interacting with an ``ESCItem``, the game character will automatically +## walk to an ``ESCLocation`` that is created as a child of an ``ESCItem``.[br] +##[br] +## By selecting the "Is Exit" checkbox when you create an ``ESCItem`` +## node, Escoria will look for an ``:exit_scene`` event in the attached script file. +## Any commands you place in the ``:exit_scene`` event will be run when the player +## chooses to "use" the exit - for example, saying a goodbye, or running a +## cutscene. Place a ``change_scene`` command inside this event to move the +## character to the next room. +class_name ESCItem +extends Area2D + + +## Emitted when the mouse has entered this item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |item|`Variant`|Item that emitted the signal.|yes|[br] +## [br] +signal mouse_entered_item(item) + +## Emitted when the mouse has exited this item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |item|`Variant`|Item that emitted the signal.|yes|[br] +## [br] +signal mouse_exited_item(item) + +## Emitted when the item was left clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_left_clicked_item(global_id) + +## Emitted when the item was double clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_double_left_clicked_item(global_id) + +## Emitted when the item was right clicked[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |global_id|`Variant`|ID of this item|yes|[br] +## [br] +signal mouse_right_clicked_item(global_id) + +## Emitted when the item walked to a destination[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |walk_context|`Variant`|The walk context of the command|yes|[br] +## [br] +signal arrived(walk_context) + +## Emitted when the item started talking. +## [br] +signal started_talking + +## Emitted when the item stopped talking. +## [br] +signal stopped_talking + +## Emitted when the item started walking. +## [br] +signal started_walking + +## Emitted when the item stopped walking. +## [br] +signal stopped_walking + + +## List of forbidden characters in global_ids +const FORBIDDEN_CHARACTERS: String = "['\"]" + + +## The global ID of this item +@export var global_id: String + +## The ASHES script for this item +@export_file("*.esc", "*.ash") var esc_script: String + +## The node that references the camera position and zoom if this item is used +## as a camera target +@export var camera_node: NodePath + +@export_group("Display") + +## The name for the tooltip of this item. +@export var tooltip_name: String + +@export_group("","") +@export_group("Item Behavior") + +## Whether this item is movable. A movable item will be scaled with the terrain +## and be moved with commands like ``teleport`` and ``turn_to``. +@export var is_movable: bool = false + +## Color used for dialogs if that item talks. +@export var dialog_color: Color = Color(1,1,1,1) + +## Default action to use if object is not in the inventory. +@export var default_action: String + +## Actions in this list make the item act as a source object and wait for a +## second clicked target object before running the action, typically +## `USE <X> WITH <Y>` or `GIVE <X> TO <Y>`. +@export var actions_requiring_target_object: PackedStringArray = [] + +## If enabled, the ASHES script may have an :exit_scene event to manage scene changes. +## For simple exits that do not require scripted actions, the ESCExit node may be +## preferred. +@export var is_exit: bool + +## Defines this item as acting as a trigger if enabled.[br] +## Allows using specific events (defined in trigger_in_verb and trigger_out_verb +## properties) in ASHES scripts. +@export var is_trigger: bool + +## Event name that is activated when another item enters the area defined by the +## trigger item. By default, "trigger_in". +@export var trigger_in_verb: String = "trigger_in" + +## Event name that is activated when another item exits the area defined by the +## trigger item. By default, "trigger_out". +@export var trigger_out_verb: String = "trigger_out" + +## Defines whether the player can interact with this item. If false, the item +## will not react to inputs and mouse hovers. +@export var is_interactive: bool = true + + +@export_subgroup("Hover Behavior") + +## Defines whether Escoria will manage a specific hover behavior when the item +## is focused. All options below can be used together.[br] +## This can also be expanded or overriden in your ESCGame implementation +## (in methods ``element_focused()`` and ``element_unfocused()``). +@export var hover_enabled: bool = false + +## If hover is enabled, applies this color modulation on the ESCItem sprite. +@export var hover_modulate: Color = Color.WHITE + +## If hover is enabled, replaces this ESCItem sprite texture by this one. +@export var hover_texture: Texture2D = null + +## If hover is enabled, applies this shader to the ESCItem sprite. +@export var hover_shader: ShaderMaterial = null + +@export_subgroup("","") + +@export_group("","") +@export_group("Player behavior on arrival") + +## Whether player character orients towards 'interaction_angle' as it arrives at +## the item's interaction position. +@export var player_orients_on_arrival: bool = true + +## If 'player_orients_on_arrival' is enabled, let the player character turn to +## this angle when it arrives at the item's interaction position. +@export var interaction_angle: int + +@export_group("","") +@export_group("Inventory") + +## Default action to use if object is in the inventory +@export var default_action_inventory: String + +## If enabled, combination must be done in the way it is written in ASHES script +## ie. :use ON_ITEM[br] +## If disabled, combination will be tried in the other way. +@export var combine_is_one_way: bool = false + +## If enabled, then the object must have been picked up before using it. +## Keep disabled for items in the background, such as buttons. +@export var use_from_inventory_only: bool = false + +## The visual representation for this item when it's in the inventory +@export var inventory_texture: Texture2D = null: + get = _get_inventory_texture + +## The visual representation for this item when it's in the inventory and hovered +@export var inventory_texture_hovered: Texture2D = null: + get = _get_inventory_texture_hovered + +@export_group("","") +@export_group("Movement") + +## If enabled, terrain scaling will not be applied and +## node will remain at the scale set in the scene. +@export var dont_apply_terrain_scaling: bool = false + +## Speed of this item if movable +@export var speed: int = 300 + +## Speed damp of this item if movable +@export var v_speed_damp: float = 1.0 + +@export_group("","") +@export_group("Animations") + +##Â Animations resource for the item (walking, idling...) +@export var animations: ESCAnimationResource: + set = set_animations + +## The node used to play animations +@export var animation_player_node: NodePath = "": + set = _set_animation_player_node + +@export_group("","") +@export_group("Custom actions") + +## Custom data dictionary to ease customization and custom command creation. +## Avoid name collision using proper key names. +@export var custom_data: Dictionary = {}: + get = get_custom_data, + set = set_custom_data + + +@export_group("","") + +## Reference to the animation node (null if none was found) +var animation_sprite = null + +## Reference to the current terrain +var terrain: ESCTerrain + +## Reference to this items collision shape node +var collision: Node + + +## Reference to the sprite node +var _sprite_node: Node = null + +## The movable subnode +var _movable: ESCMovable = null + +## The identified animation player +var _animation_player: ESCAnimationPlayer = null + +## Original sprite color modulation restored when hover ends. +var _previous_color_modulate: Color + +## Original sprite texture restored when hover ends. +var _previous_texture: Texture2D = null + +## Whether to force regsitration with the object manager. Defaults to false. +var _force_registration: bool = false + +## Warnings for scene. +var _scene_warnings: PackedStringArray = [] + + +# Add the movable node, connect signals, detect child nodes +# and register this item +func _ready(): + self.process_mode = Node.PROCESS_MODE_PAUSABLE + + _detect_children() + + # We add ourselves to this group so we can easily get a reference to all + # items in a scene tree. + if not Engine.is_editor_hint(): + add_to_group(escoria.GROUP_ITEM_CAN_COLLIDE) + if is_trigger: + add_to_group(escoria.GROUP_ITEM_TRIGGERS) + + validate_animations(animations) + validate_exported_parameters() + + if not input_event.is_connected(_on_input_event): + input_event.connect(_on_input_event) + if not mouse_exited.is_connected(_on_mouse_exited): + mouse_exited.connect(_on_mouse_exited) + + # Register and connect all elements to Escoria backoffice. + if not Engine.is_editor_hint(): + + if is_movable: + _movable = ESCMovable.new() + add_child(_movable) + + if not escoria.event_manager.event_finished.is_connected(_update_terrain): + escoria.event_manager.event_finished.connect(_update_terrain) + + _force_registration = true if escoria.save_manager.is_loading_game else false + escoria.object_manager.register_object( + ESCObject.new( + global_id, + self + ), + null, + _force_registration + ) + + terrain = escoria.room_terrain if is_instance_valid(escoria.room_terrain) else null + + if escoria.object_manager.get_object(global_id).state == ESCObject.STATE_DEFAULT \ + and get_animation_player() != null: + escoria.object_manager.get_object(global_id) \ + .set_state(get_animation_player().get_animation()) + if is_movable: + escoria.object_manager.get_object(global_id).node \ + ._movable.last_dir = animations.get_direction_id_from_animation_name( + get_animation_player().get_animation() + ) + + + if escoria.object_manager.get_object(global_id).state == ESCObject.STATE_DEFAULT \ + and escoria.object_manager.get_object(global_id).node.get_animation_player() != null: + escoria.object_manager.get_object(global_id) \ + .set_state(get_animation_player().get_animation()) + if is_movable: + escoria.object_manager.get_object(global_id).node._movable.last_dir = 1 + #animations.get_direction_id_from_animation_name( + #.get_animation_player().get_animation() + #) + + if !is_trigger: + if not self.is_connected( + "mouse_entered_item", + escoria.inputs_manager._on_mouse_entered_item + ): + mouse_entered_item.connect( + escoria.inputs_manager._on_mouse_entered_item + ) + if not self.is_connected( + "mouse_exited_item", + escoria.inputs_manager._on_mouse_exited_item + ): + mouse_exited_item.connect( + escoria.inputs_manager._on_mouse_exited_item + ) + if not self.is_connected( + "mouse_left_clicked_item", + escoria.inputs_manager._on_mouse_left_clicked_item + ): + mouse_left_clicked_item.connect( + escoria.inputs_manager._on_mouse_left_clicked_item + ) + if not self.is_connected( + "mouse_double_left_clicked_item", + escoria.inputs_manager._on_mouse_left_double_clicked_item + ): + mouse_double_left_clicked_item.connect( + escoria.inputs_manager._on_mouse_left_double_clicked_item + ) + if not self.is_connected( + "mouse_right_clicked_item", + escoria.inputs_manager._on_mouse_right_clicked_item + ): + mouse_right_clicked_item.connect( + escoria.inputs_manager._on_mouse_right_clicked_item + ) + else: # Item is a trigger + if not self.is_connected("area_entered", _element_entered): + area_entered.connect(_element_entered) + if not self.is_connected("area_exited", _element_exited): + area_exited.connect(_element_exited) + if not self.is_connected("body_entered", _element_entered): + body_entered.connect(_element_entered) + if not self.is_connected("body_exited", _element_exited): + body_exited.connect(_element_exited) + + # If object can be in the inventory, set default_action_inventory to same as + # default_action, if default_action_inventory is not set + if use_from_inventory_only and default_action_inventory.is_empty(): + default_action_inventory = default_action + + # Perform a first terrain scaling if we have to. + if (not is_exit or dont_apply_terrain_scaling) and is_movable: + _movable.last_scale = scale + _movable.update_terrain() + +## For trigger items, enable monitoring to detect collisions.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func connect_trigger_events(): + assert(is_trigger) + self.monitoring = true + + +## Validates the various exported parameters so we get immediate crash.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func validate_exported_parameters() -> void: + var regex = RegEx.new() + regex.compile(FORBIDDEN_CHARACTERS) + var result = regex.search(global_id) + if result: + ESCSafeLogging.log_error( + self, + "Forbidden character in global_id %s (path: %s)" + % [global_id, get_path()] + ) + if global_id.is_empty(): + ESCSafeLogging.log_error( + self, + "global_id of item is empty (node name : %s, path: %s)" + % [name, get_path()] + ) + +## For trigger items, disable monitoring to disable collisions detection.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func disconnect_trigger_events(): + assert(is_trigger) + self.monitoring = false + + +## Mouse exited happens on any item that mouse cursor exited, even those UNDER the top level of overlapping stack.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _on_mouse_exited(): + if escoria.inputs_manager.hover_stack.has(self): + escoria.inputs_manager.hover_stack.erase_item(self) + escoria.inputs_manager.unset_hovered_node(self) + _apply_unhover_behavior() + +## Special helper class used to sort stacked items by z-index. +class HoverStackSorter: + static func sort_ascending_z_index(a, b): + if a.z_index < b.z_index: + return true + return false + + +## Manage input events on the item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |_viewport|`Object`|the viewport node the event entered|yes|[br] +## |event|`InputEvent`|Input event delivered to the item.|yes|[br] +## |_shape_idx|`int`|Index of the collision shape within the item that received the event.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _on_input_event(_viewport: Object, event: InputEvent, _shape_idx: int): + if event is InputEventMouseMotion: + var physics2d_dss: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state + var params := PhysicsPointQueryParameters2D.new() + params.position = get_global_mouse_position() + params.collision_mask = 0x7FFFFFFF + params.collide_with_areas = true + var colliding: Array = physics2d_dss.intersect_point(params, 32) + var colliding_nodes = [] + for c in colliding: + if c.collider.get("global_id") \ + and escoria.action_manager.is_object_actionable(c.collider.global_id): + colliding_nodes.push_back(c.collider) + + if colliding_nodes.is_empty(): + return + colliding_nodes.sort_custom(Callable(HoverStackSorter, "sort_ascending_z_index")) + escoria.inputs_manager.hover_stack.clear() + escoria.inputs_manager.hover_stack.add_items(colliding_nodes) + escoria.inputs_manager.set_hovered_node(colliding_nodes.back()) + +## Manage mouse button clicks on this item by sending out signals[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |input_event|`InputEvent`|Triggered event|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _unhandled_input(input_event: InputEvent) -> void: + # If this is a trigger, then escoria.inputs_manager is not wired up to + # receive the signals this function might dispatch. In particular, + # calling get_tree().set_input_as_handled() unnecessarily will prevent + # the ESCBackground from being able to process the event. + # See https://github.com/godot-escoria/escoria-issues/issues/147. + if is_trigger: + return + + var event = input_event + # Note that event could be InputEventMouseButton, InputEventJoypadButton, + # or something else. As such, the value of the `button_index` property + # must be read in the context of the type of input event. + if input_event is InputEventJoypadButton: + if not input_event.is_action_pressed(escoria.inputs_manager.ESC_UI_PRIMARY_ACTION): + return + + # For now, rather than refactor input handling to be more generic + # to accommodate gamepad support, we create a synthetic mouse event + # based on the InputEventJoypadButton. + event = InputEventMouseButton.new() + event.button_index = MOUSE_BUTTON_LEFT + event.double_click = false + event.button_pressed = true + # ESCActionManager expects to read the position off of the event. + event.position = get_global_mouse_position() + + if event is InputEventMouseButton and event.is_pressed(): + if not escoria.current_state == escoria.GameState.DEFAULT: + ESCSafeLogging.log_info( + self, + "Current game state doesn't accept interactions." + ) + return + var p = get_global_mouse_position() + if _is_in_shape(p) and escoria.action_manager.is_object_actionable(global_id): + if event.double_click and event.button_index == MOUSE_BUTTON_LEFT: + mouse_double_left_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + elif event.button_index == MOUSE_BUTTON_LEFT: + mouse_left_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + elif event.button_index == MOUSE_BUTTON_RIGHT: + mouse_right_clicked_item.emit(self, event) + get_viewport().set_input_as_handled() + + +## To display warnings in the scene tree should there be any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `PackedStringArray` value containing configuration warnings +func _get_configuration_warnings(): + validate_animations(animations) + return _scene_warnings + +## Tests whether given point is inside a shape.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |position|`Vector2`|point coordinates to test|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func _is_in_shape(position: Vector2) -> bool: + var params := PhysicsPointQueryParameters2D.new() + params.position = position + params.collision_mask = 2147483647 + params.collide_with_areas = true + var colliders = get_world_2d().direct_space_state.intersect_point( + params, + 32 + ) + for _owner in get_shape_owners(): + for _shape_id in range(0, shape_owner_get_shape_count(_owner)): + for _collider in colliders: + if _collider.collider == self and _collider.shape == _shape_id: + return true + return false + + +## Validates the ESCAnimationResource if it exists. Note that we pass in the ESCAnimationResource as an argument so that it can also be used to validate an ESCAnimationResource prior to being set.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |animations_resource|`ESCAnimationResource`|Animation resource to validate before using it on this item.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func validate_animations(animations_resource: ESCAnimationResource) -> void: + if not is_instance_valid(animations_resource): + return + + # This initialization must always be here since this is a tool script. + _scene_warnings = [] + + if is_instance_valid(animations_resource): + _validate_animations_property_all_not_null(animations_resource.dir_angles, "dir_angles") + + var num_dir_angles = animations_resource.dir_angles.size() + + if animations_resource.directions.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'directions' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.directions.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.directions, "directions") + + if animations_resource.idles.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'idles' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.idles.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.idles, "idles") + + if animations_resource.speaks.size() != num_dir_angles: + _scene_warnings.append("%s animation angles specified but %s 'speaks' animation(s) given. [%s]" \ + % [num_dir_angles, animations_resource.speaks.size(), _get_identifier_as_key_value()]) + else: + _validate_animations_property_all_not_null(animations_resource.speaks, "speaks") + + if Engine.is_editor_hint(): + update_configuration_warnings() + elif _scene_warnings.size() > 0: + ESCSafeLogging.log_error( + self, + ", ".join(_scene_warnings) + ) + + +## Setter for the animations property.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |p_animations|`ESCAnimationResource`|the ESCAnimationResource to set.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_animations(p_animations: ESCAnimationResource) -> void: + if p_animations == null: + return + + animations = p_animations + + if not animations.changed.is_connected(validate_animations): + animations.changed.connect(validate_animations) + + +## The animation player node[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the animation player node. (`Node`) +func get_animation_player() -> Node: + if _animation_player == null: + var player_node_path = animation_player_node + if player_node_path.is_empty(): + for child in self.get_children(): + if child is AnimatedSprite2D or \ + child is AnimationPlayer: + player_node_path = child.get_path() + if player_node_path.is_empty(): + ESCSafeLogging.log_debug( + self, + "Can not find animation_player or animated sprite for %s." % global_id + ) + elif not has_node(player_node_path): + ESCSafeLogging.log_debug( + self, + "Can not find animation_player node at path %s for %s." % [player_node_path, global_id] + ) + else: + _animation_player = ESCAnimationPlayer.new( + get_node(player_node_path) + ) + return _animation_player + + +## Return the position the player needs to walk to to interact with this item. That can either be a direct Position2D child or a collision shape[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Vector2` value. (`Vector2`) +func get_interact_position() -> Vector2: + var pos_2d_count: int = 0 + var pos_2d_position = null + + var esclocation_count = 0 + var esclocation_position = null + + var interact_count = 0 + var interact_position = null + + for c in get_children(): + if c is Marker2D: + # Identify any Postion2D nodes + if c is ESCLocation: + esclocation_count += 1 + esclocation_position = c.global_position + elif c is ESCInteractionLocation: + interact_count += 1 + interact_position = c.global_position + else: + # This will catch all other Position2D related nodes + # including dialog locations and native Position2D nodes. + pos_2d_count += 1 + pos_2d_position = c.global_position + + if interact_position == null and \ + esclocation_position == null and is_instance_valid(collision): + ESCSafeLogging.log_warn( + self, + "No ESCLocation found to walk to for object " + + "%s. Middle of collision shape will be used." % global_id) + return collision.global_position + + if interact_count > 0: + if interact_count > 1: + ESCSafeLogging.log_warn( + self, + "Multiple ESCInteractionLocations found to walk to for " + + "object %s. Last one will be used." % global_id) + return interact_position + + if esclocation_count > 1: + ESCSafeLogging.log_warn( + self, + "Multiple ESClocations found to walk to for object " + + "%s. Last one will be used." % global_id) + return esclocation_position + + +## React to the mouse entering the item by emitting the respective signal[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func mouse_entered(): + if escoria.action_manager.is_object_actionable(global_id): + mouse_entered_item.emit(self) + _apply_hover_behavior() + + + +## React to the mouse exiting the item by emitting the respective signal[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func do_mouse_exited(): + mouse_exited_item.emit(self) + + +# Another item (e.g. the player) has entered this item +# +# #### Parameters +# +# - body: Other object that has entered the item +func _element_entered(body): + if body is ESCBackground or body.get_parent() is ESCBackground: + return + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_IN, + [global_id, body.global_id, trigger_in_verb] + ) + + +# Another item (e.g. the player) has exited this element +# +# #### Parameters +# +# - body: Other object that has exited the item +func _element_exited(body): + if body is ESCBackground or body.get_parent() is ESCBackground: + return + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_OUT, + [global_id, body.global_id, trigger_out_verb] + ) + + +## Use the movable node to teleport this item to the target item[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |target|`Node`|Target node to teleport to|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func teleport(target: Node) -> void: + if is_movable: + _movable.teleport(target) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot \"teleport\". Its \"is_movable\" parameter is false." %self + ) + + +## Use the movable node to teleport this item to the target position[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |target|`Vector2`|Vector2 position to teleport to|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func teleport_to(target: Vector2) -> void: + if is_movable: + _movable.teleport_to(target) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot \"teleport_to\". Its \"is_movable\" parameter is false." %self + ) + + +## Use the movable node to make the item walk to the given position[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |pos|`Vector2`|Position to walk to|yes|[br] +## |p_walk_context|`ESCWalkContext`|Walk context to use|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func walk_to(pos: Vector2, p_walk_context: ESCWalkContext = null) -> void: + if is_movable: + started_walking.emit() + _movable.walk_to(pos, p_walk_context) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"walk_to\". Its \"is_movable\" parameter is false." %self + ) + + +## Stop the movable node immediately and remain where it is at this moment, or teleport it directly at destination position if 'to_target' is true.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |to_target|`bool`|if true, the movable node is teleport directly at its target destination|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func stop_walking_now(to_target: bool = false) -> void: + if is_movable: + var where: Vector2 = position + if to_target: + where = _movable.walk_destination + stopped_walking.emit() + _movable.walk_stop(where) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"stop_walking_now\". Its \"is_movable\" parameter is false." %self + ) + + +## Set the moving speed[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |speed_value|`int`|Set the new speed|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_velocity(speed_value: int) -> void: + speed = speed_value + + +## Checks whether this item moved[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func has_moved() -> bool: + return _movable.moved if is_movable else false + + +## Checks whether this item has a sprite.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func has_sprite() -> bool: + if _sprite_node != null: + return true + + for child in self.get_children(): + if child is AnimatedSprite2D or child is Sprite2D: + return true + return false + +## Return the sprite node.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Node` value. (`Node`) +func get_sprite() -> Node: + if _sprite_node == null: + for child in self.get_children(): + if child is AnimatedSprite2D or child is Sprite2D: + _sprite_node = child + if _sprite_node == null: + ESCSafeLogging.log_error( + self, + "No sprite node found in the scene %s." % get_path() + ) + return _sprite_node + + +## Set the angle[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |deg|`int`|The angle degree to set|yes|[br] +## |wait|`float`|Wait this amount of seconds until continuing with turning around|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_angle(deg: int, wait: float = 0.0): + if is_movable: + _movable.set_angle(deg, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"set_angle\". Its \"is_movable\" parameter is false." % self + ) + + +## Set the direction id[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |direction_id|`int`|The direction id|yes|[br] +## |wait|`float`|Wait this amount of seconds until continuing with turning around|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_direction(direction_id: int, wait: float = 0.0): + if is_movable: + _movable.set_direction(direction_id, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"set_direction\". Its \"is_movable\" parameter is false." % self + ) + + +## Turn to face another object[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |object|`Node`|Node whose position the item should face.|yes|[br] +## |wait|`float`|Optional delay in seconds before execution continues after turning.|no|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func turn_to(object: Node, wait: float = 0.0): + if is_movable: + _movable.turn_to(object, wait) + else: + ESCSafeLogging.log_warn( + self, + "Node %s cannot use \"turn_to\". Its \"is_movable\" parameter is false." % self + ) + + +## Check everything is in place to play talk animations. A warning indicating the problem is printed in console, if necessary.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `bool` value. (`bool`) +func check_talk_possible() -> bool: + if is_movable and (_movable.last_dir < 0 \ + or _movable.last_dir >= animations.speaks.size()): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"last_dir\" parameter is invalid: %s." \ + % [self, _movable.last_dir] + ) + return false + if not is_instance_valid(animations): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"animations\" parameter is empty." \ + % self + ) + return false + if animations.speaks.size() == 0: + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its \"animations.speaks\" array is empty." \ + % self + ) + return false + if not get_animation_player(): + ESCSafeLogging.log_warn( + self, + "Node %s cannot talk. Its animation player can't be found." \ + % self + ) + return false + return true + + +## Play the talking animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func start_talking(): + if not check_talk_possible(): + return + + var animation_player = get_animation_player() + + if animation_player.is_playing(): + animation_player.stop() + + if is_movable and animations.speaks[_movable.last_dir].mirrored \ + and not _movable.is_mirrored: + _sprite_node.scale.x *= -1 + + var animation_direction = _movable.last_dir if is_movable else 0 + animation_player.play( + animations.speaks[animation_direction].animation + ) + started_talking.emit(animation_direction) + + +## Stop playing the talking animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func stop_talking(): + if not check_talk_possible(): + return + + var animation_player = get_animation_player() + + if animation_player.is_playing(): + animation_player.stop() + + if is_movable: + if animations.speaks[_movable.last_dir].mirrored \ + and not _movable.is_mirrored: + # Allow this function to be called multiple times without setting + # the direction incorrectly + if _sprite_node.scale.x < 1: + _sprite_node.scale.x *= -1 + + animation_player.play( + animations.idles[_movable.last_dir].animation + ) + else: + animation_player.play( + animations.idles[0].animation + ) + stopped_talking.emit() + + +## Replay the last idle animation[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func update_idle(): + get_animation_player().play( + animations.idles[_movable.last_dir].animation + ) + + +## Return the camera position if a camera_position_node exists or the global position of the player[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a `Node` value. (`Node`) +func get_camera_node() -> Node: + if has_node(camera_node): + ESCSafeLogging.log_debug( + self, + "Camera3D node found - directing camera to the camera_node on %s." + % global_id + ) + return get_node(camera_node) + return self + + +## Detect the child nodes and set respective references.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _detect_children() -> void: + # Initialize collision variable. + for c in get_children(): + if c is CollisionShape2D or c is CollisionPolygon2D: + collision = c + + +## Update the terrain when an event finished[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |rc|`int`|Return code of the event that just finished|yes|[br] +## |event_name|`String`|Event that just finished to provide to update terrain call.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _update_terrain(_rc: int, event_name: String) -> void: + if is_movable: + _movable.update_terrain(event_name) + +## Godot method override to provide more information about this object.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _get_property_list(): + var properties = [] + properties.append({ + "name": "animations", + "type": TYPE_OBJECT, + "hint": PROPERTY_HINT_RESOURCE_TYPE, + "hint_string": "ESCAnimationResource" + }) + return properties + + +## Set the node path to the animation player[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |node_path|`NodePath`|Path to the player node|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _set_animation_player_node(node_path: NodePath): + if not Engine.is_editor_hint(): + return + + if node_path.is_empty(): + animation_player_node = node_path + return + + assert(has_node(node_path), "Node with path %s not found" % node_path) + assert( + get_node(node_path) is AnimatedSprite2D or \ + get_node(node_path) is AnimationPlayer, + "Selected node has to be an AnimatedSprite2D or AnimationPlayer node" + ) + + animation_player_node = node_path + + +## Either the set inventory texture or the texture of a TextureRect found as a child if it is null[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns either the set inventory texture or the texture of a TextureRect found as a child if it is null. (`Texture2D`) +func _get_inventory_texture() -> Texture2D: + if inventory_texture == null: + for c in get_children(): + if c is TextureRect or c is Sprite2D: + return c.texture + return null + + return inventory_texture + + +func _get_inventory_texture_hovered() -> Texture2D: + if inventory_texture_hovered == null: + return _get_inventory_texture() + + return inventory_texture_hovered + +## Checks whether the given ESCAnimationResource property array has all non-null entries, and adds to the scene's warnings if not[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |property|`Array`|ESCAnimationResource property. Must be an array.|yes|[br] +## |property_name|`String`|the name of the property being passed in.|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _validate_animations_property_all_not_null(property: Array, property_name: String) -> void: + var has_empty_entry: bool = false + + for item in property: + if item == null: + has_empty_entry = true + break + + if has_empty_entry: + _scene_warnings.append("At least one entry in '%s' is empty. [%s]" % [property_name, _get_identifier_as_key_value()]) + + +## The global ID as a key/value pair. If none is specified, use the node name. Used to tag messages.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the global ID as a key/value pair. If none is specified, use the node name. Used to tag messages. (`String`) +func _get_identifier_as_key_value() -> String: + if self.global_id: + return "global_id: %s" % self.global_id + + return "node: %s" % get_name() + +## Applies hover behavior on this ESCItem: if hover is enabled, applies the defined hover color modulate and/or hover texture and/or hover shader on the Sprite2D node, if any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _apply_hover_behavior() -> void: + if not hover_enabled: + return + if has_sprite(): + var sprite = get_sprite() + _previous_color_modulate = modulate + sprite.modulate = hover_modulate + if sprite is Sprite2D: + if hover_texture != null: + _previous_texture = sprite.texture + sprite.texture = hover_texture + if hover_shader != null: + sprite.material = hover_shader + +## Applies unhover behavior on this ESCItem: if hover is enabled, reverts back to normal the defined hover color modulate and/or hover texture and/or hover shader on the Sprite2D node, if any.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func _apply_unhover_behavior() -> void: + if not hover_enabled: + return + if has_sprite(): + var sprite = get_sprite() + sprite.modulate = _previous_color_modulate + if sprite is Sprite2D: + if hover_texture != null: + sprite.texture = _previous_texture + if hover_shader != null: + sprite.material = null + + +## Whether the item is currently moving.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns true if the player is currently moving, false otherwise. (`bool`) +func is_moving() -> bool: + return _movable.task != ESCMovable.MovableTask.NONE if is_movable else false + +## The number of defined animation directions for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns the number of defined animation directions for this ESCItem. (`int`) +func get_directions_quantity() -> int: + return animations.dir_angles.size() + + +## A Dictionary containing custom data defined for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## None. +## [br] +## #### Returns[br] +## [br] +## Returns a Dictionary containing custom data defined for this ESCItem. (`Dictionary`) +func get_custom_data() -> Dictionary: + return custom_data + +## Sets the `custom_data` Dictionary containing custom data defined for this ESCItem.[br] +## [br] +## #### Parameters[br] +## [br] +## | Name | Type | Description | Required? |[br] +## |:-----|:-----|:------------|:----------|[br] +## |data|`Dictionary`|Custom data dictionary to assign to this item (an empty dictionary is used when `null`).|yes|[br] +## [br] +## #### Returns[br] +## [br] +## Returns nothing. +func set_custom_data(data: Dictionary) -> void: + custom_data = data if (data != null) else {} diff --git a/addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json b/addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json new file mode 100644 index 0000000..5520f01 --- /dev/null +++ b/addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json @@ -0,0 +1 @@ +{"last_engine_version":"4.7.1-stable (voidlinux)","last_engine_version_hex":263937,"migration_done":true}
\ No newline at end of file diff --git a/addons/escoria-core/game/core-scripts/resources/esc_animationname.gd b/addons/escoria-core/game/core-scripts/resources/esc_animationname.gd index 482695f..a679286 100644 --- a/addons/escoria-core/game/core-scripts/resources/esc_animationname.gd +++ b/addons/escoria-core/game/core-scripts/resources/esc_animationname.gd @@ -1,7 +1,7 @@ @tool ## Class defining an animation to use for an angle. -extends Resource class_name ESCAnimationName +extends Resource ## Name of the animation @export var animation: String diff --git a/addons/escoria-core/game/core-scripts/resources/esc_animationresource.gd b/addons/escoria-core/game/core-scripts/resources/esc_animationresource.gd index 337013e..26982b0 100644 --- a/addons/escoria-core/game/core-scripts/resources/esc_animationresource.gd +++ b/addons/escoria-core/game/core-scripts/resources/esc_animationresource.gd @@ -1,8 +1,8 @@ @tool ## Resource containing all defined animations and angles for character ## movement.[br] -extends Resource class_name ESCAnimationResource +extends Resource ## Array containing the different angles available for animations.[br] ## Each angle is defined by an array [start_angle, angle_size]. diff --git a/addons/escoria-core/game/core-scripts/resources/esc_directionangle.gd b/addons/escoria-core/game/core-scripts/resources/esc_directionangle.gd index 0050dc4..deab0f3 100644 --- a/addons/escoria-core/game/core-scripts/resources/esc_directionangle.gd +++ b/addons/escoria-core/game/core-scripts/resources/esc_directionangle.gd @@ -1,7 +1,7 @@ @tool ## Class defining an angle, with a start angle (between 0 and 360) and a size. -extends Resource class_name ESCDirectionAngle +extends Resource ## Start angle of the directional angle. @export var angle_start: int diff --git a/addons/escoria-core/game/core-scripts/resources/esc_resource_descriptor.gd b/addons/escoria-core/game/core-scripts/resources/esc_resource_descriptor.gd index 72e2cce..2da6e92 100644 --- a/addons/escoria-core/game/core-scripts/resources/esc_resource_descriptor.gd +++ b/addons/escoria-core/game/core-scripts/resources/esc_resource_descriptor.gd @@ -1,6 +1,6 @@ ## Describes a resource for use in the resource cache. -extends Resource class_name ESCResourceDescriptor +extends Resource ## The resource being described. var res diff --git a/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd b/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd index ec78fe4..9b4ba24 100644 --- a/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd +++ b/addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd @@ -48,15 +48,18 @@ var _add_inventory: InventoryAddCommand var _transition: TransitionCommand var _hide_menu: HideMenuCommand var _camera_set_target: CameraSetTargetCommand +var _camera_set_limits: CameraSetLimitsCommand var _change_scene: ChangeSceneCommand var _enable_terrain: EnableTerrainCommand var _set_active: SetActiveCommand var _set_active_if_exists: SetActiveIfExistsCommand +var _set_animations: SetAnimationsCommand var _set_interactive: SetInteractiveCommand var _set_item_custom_data: SetItemCustomDataCommand var _teleport_pos: TeleportPosCommand var _set_direction: SetDirectionCommand var _set_global: SetGlobalCommand +var _set_speed: SetSpeedCommand var _set_state: SetStateCommand var _stop_snd: StopSndCommand var _play_snd: PlaySndCommand @@ -81,6 +84,7 @@ func _init(): _transition = TransitionCommand.new() _hide_menu = HideMenuCommand.new() _camera_set_target = CameraSetTargetCommand.new() + _camera_set_limits = CameraSetLimitsCommand.new() _change_scene = ChangeSceneCommand.new() _enable_terrain = EnableTerrainCommand.new() _set_active = SetActiveCommand.new() @@ -88,8 +92,10 @@ func _init(): _set_interactive = SetInteractiveCommand.new() _set_item_custom_data = SetItemCustomDataCommand.new() _teleport_pos = TeleportPosCommand.new() + _set_animations = SetAnimationsCommand.new() _set_direction = SetDirectionCommand.new() _set_global = SetGlobalCommand.new() + _set_speed = SetSpeedCommand.new() _set_state = SetStateCommand.new() _stop_snd = StopSndCommand.new() _play_snd = PlaySndCommand.new() @@ -297,6 +303,9 @@ func load_game(id: int): ) return + # Stop all musics and sounds + escoria.main.stop_all_sounds() + # Disconnect all trigger areas in the current room so that they don't # trigger after room is loaded (eg: when player was in a trigger area, # trigger_out won't fire after loading the game) @@ -318,14 +327,12 @@ func load_game(id: int): "Loading savegame %s" % str(id) ) is_loading_game = true - escoria.current_state = escoria.GAME_STATE.LOADING + escoria.current_state = escoria.GameState.LOADING var save_game: ESCSaveGame = ResourceLoader.load(save_file_path) escoria.settings_manager.load_settings_from_dict(save_game.settings) - escoria.settings_manager.load_settings_from_dict(save_game.settings) - var plugin_config = ConfigFile.new() plugin_config.load("res://addons/escoria-core/plugin.cfg") var escoria_version = plugin_config.get_value("plugin", "version") @@ -385,11 +392,22 @@ func load_game(id: int): # Set save manager "is_loading_game" var to false since we're now done # Hide main and pause menus - escoria.game_scene.hide_main_menu() + escoria.game_scene.hide_main_menu() # THIS SETS escoria.current_state = DEFAULT !! escoria.game_scene.unpause_game() + escoria.current_state = escoria.GameState.LOADING + escoria.room_manager.change_scene_to_file(save_game.main.current_scene_filename, false) + # Pop sound and music data from the savegame + var sound_music_saved_data: Dictionary = {} + for object_id in save_game.objects: + if object_id in ESCObjectManager.RESERVED_OBJECTS: + sound_music_saved_data[object_id] = save_game.objects[object_id] + for object_id in sound_music_saved_data: + save_game.objects.erase(object_id) + + # Load the object _load_savegame_objects(save_game.objects) escoria.globals_manager.clear() @@ -397,7 +415,10 @@ func load_game(id: int): _load_savegame_inventory(save_game.inventory) _load_savegame_terrain_navpolys(save_game.terrain_navpolys) _load_savegame_events(save_game.events) - _transition.run(["", "in", 1.0]) + await _transition.run(["", "in", 1.0]) + + # Music and sound resume + _load_savegame_objects(sound_music_saved_data) escoria.set_game_paused(false) @@ -405,7 +426,7 @@ func load_game(id: int): escoria.action_manager.clear_current_tool() escoria.inputs_manager.input_mode = escoria.inputs_manager.INPUT_ALL is_loading_game = false - escoria.current_state = escoria.GAME_STATE.DEFAULT + escoria.current_state = escoria.GameState.DEFAULT emit_signal("game_finished_loading") @@ -425,12 +446,19 @@ func load_game(id: int): func _load_savegame_objects(savegame_objects: Dictionary): for object_id in savegame_objects: var saved_object_data = savegame_objects[object_id] - if object_id in ESCObjectManager.RESERVED_OBJECTS: # Sound players only atm + if object_id in ESCObjectManager.RESERVED_OBJECTS: + # Sound players only atm + # Sounds and music to be resumed after transition if saved_object_data.has("state") \ and saved_object_data["state"] in ["off", "default"]: _stop_snd.run([object_id]) else: - _play_snd.run([saved_object_data["state"], object_id, saved_object_data["playback_position"]]) + var playback_position: float = 0.0 + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.SAVE_SOUNDS_PLAYBACK_POSITION + ) and saved_object_data.has("playback_position"): + playback_position = saved_object_data["playback_position"] + _play_snd.run([saved_object_data["state"], object_id, playback_position]) else: if object_id == escoria.main.current_scene.global_id: _load_room_objects(object_id, saved_object_data) @@ -450,8 +478,15 @@ func _load_savegame_objects(savegame_objects: Dictionary): func _load_room_objects(room_id: String, objects_dictionary: Dictionary): escoria.logger.info(self, "Loading room '%s'" % room_id) + var camera_loaded_data = null + if objects_dictionary.has(escoria.object_manager.CAMERA): + camera_loaded_data = objects_dictionary[escoria.object_manager.CAMERA] + objects_dictionary.erase(escoria.object_manager.CAMERA) for object_id in objects_dictionary: _load_object(object_id, objects_dictionary[object_id], room_id) + # Then load camera + if camera_loaded_data != null: + _load_object(escoria.object_manager.CAMERA, camera_loaded_data, room_id) escoria.logger.info(self, "Finished loading room '%s'" % room_id) @@ -468,10 +503,17 @@ func _load_room_objects(room_id: String, objects_dictionary: Dictionary): ## #### Returns[br] ## [br] ## Returns nothing. -func _load_object(object_id: String, object_dictionary: Dictionary, room_id: String): +func _load_object(object_id: String, object_dictionary: Dictionary, _room_id: String): escoria.logger.info(self, "Loading object '%s'" % object_id) if object_id == ESCObjectManager.CAMERA: + if object_dictionary.has("limit_id"): + escoria.main.restore_camera_limit_for_room_global_id( + _room_id, + object_dictionary["limit_id"] + ) + else: + escoria.main.clear_camera_limit_id_for_room_global_id(_room_id) _camera_set_target.run([0, object_dictionary["target"]]) else: # Active @@ -482,6 +524,17 @@ func _load_object(object_id: String, object_dictionary: Dictionary, room_id: Str if object_dictionary.has("interactive") and _set_interactive.validate([object_id, object_dictionary["interactive"]]): _set_interactive.run([object_id, object_dictionary["interactive"]]) + # Animations + if object_dictionary.has("animations_resource_path") and _set_animations.validate([object_id, object_dictionary["animations_resource_path"]]): + _set_animations.run([object_id, object_dictionary["animations_resource_path"]]) + if object_dictionary.has("current_animation")\ + and _set_state.validate([ + object_id, + object_dictionary["current_animation"], + true + ]): + _set_state.run([object_id, object_dictionary["current_animation"], true]) + # State if object_dictionary.has("state") and _set_state.validate([object_id, object_dictionary["state"], true]): _set_state.run([object_id, object_dictionary["state"], true]) @@ -494,6 +547,10 @@ func _load_object(object_id: String, object_dictionary: Dictionary, room_id: Str object_dictionary["global_transform"].origin.y ]) + # Speed + if object_dictionary.has("speed"): + _set_speed.run([object_id, object_dictionary["speed"]]) + # Orientation if object_dictionary.has("last_dir"): _set_direction.run([object_id, int(object_dictionary["last_dir"]), 0.0]) @@ -590,7 +647,7 @@ func _load_savegame_events(savegame_events: Dictionary): sched_event.object ) escoria.event_manager.schedule_event( - script.events[sched_event.event_name], + script.get_event_with_target(sched_event.event_name), sched_event.timeout, sched_event.object ) @@ -609,7 +666,7 @@ func _load_savegame_events(savegame_events: Dictionary): ## #### Returns[br] ## [br] ## Returns nothing. -func _ensure_directory_exists(dir: String) -> void: +func _ensure_directory_exists(_dir: String) -> void: if not DirAccess.dir_exists_absolute(save_folder): var return_code = DirAccess.make_dir_absolute(save_folder) diff --git a/addons/escoria-core/game/core-scripts/save_data/esc_savegame.gd b/addons/escoria-core/game/core-scripts/save_data/esc_savegame.gd index b49ea0e..e8cd39d 100644 --- a/addons/escoria-core/game/core-scripts/save_data/esc_savegame.gd +++ b/addons/escoria-core/game/core-scripts/save_data/esc_savegame.gd @@ -1,6 +1,6 @@ ## Resource used for holding savegames data. -extends Resource class_name ESCSaveGame +extends Resource ## Access key for the main data last_scene_global_id. const MAIN_LAST_SCENE_GLOBAL_ID_KEY = "last_scene_global_id" diff --git a/addons/escoria-core/game/core-scripts/save_data/esc_savesettings.gd b/addons/escoria-core/game/core-scripts/save_data/esc_savesettings.gd index ccc3d1e..d899601 100644 --- a/addons/escoria-core/game/core-scripts/save_data/esc_savesettings.gd +++ b/addons/escoria-core/game/core-scripts/save_data/esc_savesettings.gd @@ -1,8 +1,8 @@ ## Resource holding game settings. Note that we call directly to ProjectSettings ## for instance variable initialization since this class is instantiated from ## escoria.gd. -extends Resource class_name ESCSaveSettings +extends Resource ## Version of ESCORIA Framework. @export var escoria_version: String diff --git a/addons/escoria-core/game/core-scripts/save_data/esc_settings_manager.gd b/addons/escoria-core/game/core-scripts/save_data/esc_settings_manager.gd index 18d4247..71eca78 100644 --- a/addons/escoria-core/game/core-scripts/save_data/esc_settings_manager.gd +++ b/addons/escoria-core/game/core-scripts/save_data/esc_settings_manager.gd @@ -293,3 +293,8 @@ func save_settings(): self, "There was an issue writing settings %s" % save_path ) + + +## Returns a custom setting by key or an optional default value. Returns null if not found and no default value is defined. +func get_custom_setting(key: String, default_value = null): + return custom_settings.get(key, default_value) diff --git a/addons/escoria-core/game/core-scripts/utils/esc_utils.gd b/addons/escoria-core/game/core-scripts/utils/esc_utils.gd index 14e4246..e1bf12e 100644 --- a/addons/escoria-core/game/core-scripts/utils/esc_utils.gd +++ b/addons/escoria-core/game/core-scripts/utils/esc_utils.gd @@ -1,6 +1,6 @@ ## A set of common utilities. -extends RefCounted class_name ESCUtils +extends RefCounted ## Convert radians to degrees.[br] ## [br] @@ -36,8 +36,7 @@ static func get_deg_from_rad(rad_angle: float): static 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 "" ## Return a string value in the correct inferred type.[br] ## [br] @@ -61,16 +60,19 @@ static func get_typed_value(value: String, type_hint = []): if regex_float.search(value): return float(value) - elif regex_int.search(value): + + if regex_int.search(value): return int(value) - elif regex_bool.search(value.to_lower()): + + if regex_bool.search(value.to_lower()): return true if value.to_lower() == "true" else false - elif (typeof(type_hint) != TYPE_ARRAY and type_hint == TYPE_ARRAY) or \ + + if (typeof(type_hint) != TYPE_ARRAY and type_hint == TYPE_ARRAY) or \ (typeof(type_hint) == TYPE_ARRAY and TYPE_ARRAY in type_hint) \ and "," in value: return value.split(",") - else: - return str(value) + + return str(value) ## Sanitize use of whitespaces in a string. Removes double whitespaces and converts tabs into space.[br] ## [br] |
