summaryrefslogtreecommitdiff
path: root/addons/escoria-core
diff options
context:
space:
mode:
Diffstat (limited to 'addons/escoria-core')
-rw-r--r--addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd80
-rw-r--r--addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd.uid1
-rw-r--r--addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.tscn57
-rw-r--r--addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd107
-rw-r--r--addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd.uid1
-rw-r--r--addons/escoria-core/game/core-scripts/behaviors/esc_movable.gd5
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/accept_input.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/anim.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/anim_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/block_say.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_push.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_push_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_limits.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_pos_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_target.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_target_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_set_zoom_height_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_shift.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/camera_shift_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/change_scene.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/change_scene_godot.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/custom.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/dec_global.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/enable_terrain.gd7
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/end_block_say.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/hide_menu.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/inc_global.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/inventory_add.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/inventory_remove.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/play_snd.gd15
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/print_internal.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/queue_event.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/queue_resource.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/rand_global.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/repeat.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/save_game.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/say.gd10
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/sched_event.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_active.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_active_if_exists.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_angle.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_animations.gd8
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_direction.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_global.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_globals.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_gui_visible.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_interactive.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_item_custom_data.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_speed.gd9
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/set_state.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/show_menu.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/slide.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/slide_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/spawn.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/stop.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/stop_snd.gd26
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/teleport.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/teleport_pos.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/transition.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/turn_to.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/wait.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/walk.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/walk_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/commands/walk_to_pos_block.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_break_counter.gd75
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_environment.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_expr.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmt.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_grammar_stmts.gd62
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter.gd249
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_interpreter_factory.gd64
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_parse_error.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_parser.gd205
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_resolver.gd12
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_scanner.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_script_builder.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/compiler/esc_token.gd12
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_action_manager.gd237
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_command_registry.gd5
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_compiler.gd23
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_event_manager.gd156
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_globals_manager.gd15
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_inventory_manager.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_object_manager.gd119
-rw-r--r--addons/escoria-core/game/core-scripts/esc/esc_room_manager.gd55
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_base_command.gd10
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_camera_base_command.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_command.gd62
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_command_argument_descriptor.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_dialog.gd42
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_dialog_option.gd16
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_event.gd20
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd52
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_events_container.gd.uid1
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_execution.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_object.gd65
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_room_container.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_room_objects.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_room_objects_key.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_room_terrains.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_scheduled_event.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_script.gd44
-rw-r--r--addons/escoria-core/game/core-scripts/esc/types/esc_statement.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc_animation_player.gd36
-rw-r--r--addons/escoria-core/game/core-scripts/esc_background.gd4
-rw-r--r--addons/escoria-core/game/core-scripts/esc_dialog_location.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc_exit.gd10
-rw-r--r--addons/escoria-core/game/core-scripts/esc_game.gd32
-rw-r--r--addons/escoria-core/game/core-scripts/esc_interaction_location.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc_item.gd81
-rw-r--r--addons/escoria-core/game/core-scripts/esc_location.gd13
-rw-r--r--addons/escoria-core/game/core-scripts/esc_player.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/esc_resource_cache.gd29
-rw-r--r--addons/escoria-core/game/core-scripts/esc_room.gd6
-rw-r--r--addons/escoria-core/game/core-scripts/esc_terrain.gd14
-rw-r--r--addons/escoria-core/game/core-scripts/esc_tooltip.gd3
-rw-r--r--addons/escoria-core/game/core-scripts/esc_walk_context.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/migrations/esc_migration.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/migrations/esc_migration_manager.gd20
-rw-r--r--addons/escoria-core/game/core-scripts/pre-4.7/.gdignore0
-rw-r--r--addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd1434
-rw-r--r--addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd1432
-rw-r--r--addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json1
-rw-r--r--addons/escoria-core/game/core-scripts/resources/esc_animationname.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/resources/esc_animationresource.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/resources/esc_directionangle.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/resources/esc_resource_descriptor.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/save_data/esc_save_manager.gd79
-rw-r--r--addons/escoria-core/game/core-scripts/save_data/esc_savegame.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/save_data/esc_savesettings.gd2
-rw-r--r--addons/escoria-core/game/core-scripts/save_data/esc_settings_manager.gd5
-rw-r--r--addons/escoria-core/game/core-scripts/utils/esc_utils.gd18
-rw-r--r--addons/escoria-core/game/esc_autoload.gd17
-rw-r--r--addons/escoria-core/game/esc_dependency_injector.gd2
-rw-r--r--addons/escoria-core/game/esc_inputs_manager.gd36
-rw-r--r--addons/escoria-core/game/esc_project_settings_manager.gd5
-rw-r--r--addons/escoria-core/game/escoria.gd39
-rw-r--r--addons/escoria-core/game/main.gd122
-rw-r--r--addons/escoria-core/game/main_scene.gd2
-rw-r--r--addons/escoria-core/game/scenes/camera_player/camera.tscn5
-rw-r--r--addons/escoria-core/game/scenes/camera_player/esc_camera.gd23
-rw-r--r--addons/escoria-core/game/scenes/camera_player/esc_camera_limits.gd2
-rw-r--r--addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd10
-rw-r--r--addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd2
-rw-r--r--addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd17
-rw-r--r--addons/escoria-core/game/scenes/esc_prompt/esc_prompt_popup.gd22
-rw-r--r--addons/escoria-core/game/scenes/inventory/inventory_ui.gd4
-rw-r--r--addons/escoria-core/game/scenes/sound/esc_ambient_player.gd2
-rw-r--r--addons/escoria-core/game/scenes/sound/esc_music_player.gd2
-rw-r--r--addons/escoria-core/game/scenes/sound/esc_sound_player.gd2
-rw-r--r--addons/escoria-core/game/scenes/sound/esc_speech_player.gd4
-rw-r--r--addons/escoria-core/game/scenes/transitions/esc_transition_player.gd8
-rw-r--r--addons/escoria-core/game/tween3.gd56
-rw-r--r--addons/escoria-core/patterns/state_machine/state.gd2
-rw-r--r--addons/escoria-core/patterns/state_machine/state_machine.gd17
-rw-r--r--addons/escoria-core/plugin.gd22
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd1879
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc3
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc8
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc14
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc9
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc12
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc5
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc8
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc14
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc6
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc5
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc9
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc6
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc10
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc10
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc11
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc5
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc15
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc17
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc11
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc4
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc6
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc3
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc8
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc11
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc2
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc2
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc10
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc10
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc7
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc12
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc6
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc5
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc11
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd16
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid1
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd32
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid0
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd15
-rw-r--r--addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/esc_parser_test.gd586
-rw-r--r--addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc9
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc11
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc10
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc7
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc9
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc6
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc9
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc9
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc5
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc5
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc5
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc41
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc2
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc6
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc6
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc7
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc5
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc5
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid1
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc9
-rw-r--r--addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid1
-rw-r--r--addons/escoria-core/testing/esc_new_game_state_test.gd38
-rw-r--r--addons/escoria-core/testing/esc_new_game_state_test.gd.uid1
-rw-r--r--addons/escoria-core/testing/esc_object_manager_test.gd79
-rw-r--r--addons/escoria-core/testing/esc_object_manager_test.gd.uid1
-rw-r--r--addons/escoria-core/testing/issue_946_room_transition_test.gd104
-rw-r--r--addons/escoria-core/testing/issue_946_room_transition_test.gd.uid1
-rw-r--r--addons/escoria-core/testing/player_angles_finder.gd20
-rw-r--r--addons/escoria-core/testing/rtl_screen_offset_testing.gd17
-rw-r--r--addons/escoria-core/tools/logging/esc_logger.gd5
-rw-r--r--addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd14
-rw-r--r--addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd18
-rw-r--r--addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd2
-rw-r--r--addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd2
-rwxr-xr-x[-rw-r--r--]addons/escoria-core/ui_library/fonts/caslonantique.ttfbin147756 -> 147756 bytes
-rw-r--r--addons/escoria-core/ui_library/inventory/esc_inventory_button.gd2
-rw-r--r--addons/escoria-core/ui_library/inventory/esc_inventory_container.gd4
-rw-r--r--addons/escoria-core/ui_library/menus/load_save/save/save_game.gd13
-rw-r--r--addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd4
-rw-r--r--addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.tscn67
-rw-r--r--addons/escoria-core/ui_library/menus/options/options.gd36
319 files changed, 8374 insertions, 952 deletions
diff --git a/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd
new file mode 100644
index 0000000..d89519d
--- /dev/null
+++ b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd
@@ -0,0 +1,80 @@
+@tool
+class_name AcceptDialogMigrate4647
+extends Window
+
+signal confirmed
+signal canceled
+
+var migrator: Migrator4647
+var required_migration: bool = false
+
+@onready var label: Label = $FlowContainer/Label
+@onready var mark_this_as_done: CheckButton = $FlowContainer/mark_this_as_done
+@onready var confirm_button: Button = $FlowContainer/HBoxContainer/confirm
+@onready var cancel_button: Button = $FlowContainer/HBoxContainer/cancel
+
+
+func _ready() -> void:
+ migrator = Migrator4647.new()
+ var action: Migrator4647.MigrationAction = migrator.check_need_upgrade_or_downgrade_scripts()
+ match action:
+ Migrator4647.MigrationAction.UPGRADE_4_7:
+ title = "Auto-upgrade Escoria-core scripts for Godot Engine 4.7.x"
+ label.text = \
+"""The version of Godot Engine you appear to be running is newer
+than the one this version of Escoria relies on (4.6.x) or this is the first time
+you run this version of Escoria.\n
+Godot Engine 4.6 and 4.7 APIs have some differences in methods that Escoria makes use of.
+Escoria can now automatically revert its impacted core scripts to makes them compatible
+with Godot Engine 4.7.\n
+Here is the list of the impacted files:
+- res://addons/escoria-core/game/core-scripts/esc_item.gd\n
+⚠️IMPORTANT⚠️: the editor will restart after this action is done.\n
+If you wish to let Escoria proceed, choose 'OK'.
+If you wish to upgrade to a newer version of Godot, choose 'Cancel'.
+If you have edited these files for your own needs, you'll need to manually fix them:
+choose 'Cancel' (see README; existing files will be backuped anyway)."""
+ required_migration = true
+ Migrator4647.MigrationAction.DOWNGRADE_4_6:
+ title = "Auto-downgrade Escoria-core scripts for Godot Engine 4.6.x"
+ label.text = \
+"""The version of Godot Engine you appear to be running is older
+than the one this version of Escoria relies on (4.7.x) or this is the first time
+you run this version of Escoria.\n
+Godot Engine 4.6 and 4.7 APIs have some differences in methods that Escoria makes use of.
+Escoria can now automatically revert its impacted core scripts to makes them compatible
+with Godot Engine 4.6.\n
+Here is the list of the impacted files:
+- res://addons/escoria-core/game/core-scripts/esc_item.gd\n
+⚠️IMPORTANT⚠️: the editor will restart after this action is done.\n
+If you wish to let Escoria proceed, choose 'OK' (a backup of the existing file will be created).
+If you wish to upgrade to a newer version of Godot, choose 'Cancel' (nothing will be done,
+but this tool will happen next time you open the editor).
+If you have edited these files for your own needs, you'll need to manually fix them:
+choose 'Cancel' (see README; existing files will be backuped anyway)."""
+ required_migration = true
+
+ _:
+ required_migration = false
+
+
+func _on_confirm_pressed() -> void:
+ if mark_this_as_done.button_pressed:
+ await migrator.save_migration_file($FlowContainer/mark_this_as_done.button_pressed)
+ else:
+ migrator.prepare_specific_godot_version(Engine.get_version_info().hex)
+ confirmed.emit()
+ hide()
+
+
+func _on_cancel_pressed() -> void:
+ await migrator.save_migration_file($FlowContainer/mark_this_as_done.button_pressed)
+ canceled.emit()
+ hide()
+
+
+func _on_mark_this_as_done_toggled(toggled_on: bool) -> void:
+ if toggled_on:
+ cancel_button.hide()
+ else:
+ cancel_button.show()
diff --git a/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd.uid b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd.uid
new file mode 100644
index 0000000..2cf55d3
--- /dev/null
+++ b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd.uid
@@ -0,0 +1 @@
+uid://cxx10dv5352wk
diff --git a/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.tscn b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.tscn
new file mode 100644
index 0000000..128c980
--- /dev/null
+++ b/addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.tscn
@@ -0,0 +1,57 @@
+[gd_scene format=3 uid="uid://ydte1xndp6p1"]
+
+[ext_resource type="Script" uid="uid://cxx10dv5352wk" path="res://addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.gd" id="1_ogeh4"]
+
+[node name="AcceptDialogMigrate4647" type="Window" unique_id=1453257784]
+oversampling_override = 1.0
+position = Vector2i(0, 36)
+size = Vector2i(720, 530)
+script = ExtResource("1_ogeh4")
+
+[node name="FlowContainer" type="VBoxContainer" parent="." unique_id=726240657]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 712.0
+offset_bottom = 517.0
+
+[node name="Label" type="Label" parent="FlowContainer" unique_id=1668065635]
+layout_mode = 2
+text = "The version of Godot Engine you appear to be running is older
+ than the one this version of Escoria relies on (4.7.x) or this is the first time
+ you run this version of Escoria.
+
+ Godot Engine 4.6 and 4.7 APIs have some differences in methods that Escoria makes use of.
+ Escoria can now automatically revert its impacted core scripts to makes them compatible
+ with Godot Engine 4.6.
+
+ Here is the list of the impacted files:
+ - res://addons/escoria-core/game/core-scripts/esc_item.gd
+
+ ⚠️IMPORTANT⚠️: the editor will restart after this action is done.
+
+ If you wish to let Escoria proceed, choose 'OK'.
+ If you wish to upgrade to a newer version of Godot, choose 'Cancel'.
+ If you have edited these files for your own needs, you'll need to manually fix them:
+ choose 'Cancel' (see README; existing files will be backuped anyway)."
+autowrap_mode = 3
+
+[node name="mark_this_as_done" type="CheckButton" parent="FlowContainer" unique_id=99402283]
+layout_mode = 2
+text = "Mark this as already done"
+
+[node name="HBoxContainer" type="HBoxContainer" parent="FlowContainer" unique_id=1123299495]
+layout_mode = 2
+theme_override_constants/separation = 55
+alignment = 1
+
+[node name="confirm" type="Button" parent="FlowContainer/HBoxContainer" unique_id=1563226882]
+layout_mode = 2
+text = "Ok"
+
+[node name="cancel" type="Button" parent="FlowContainer/HBoxContainer" unique_id=2075346076]
+layout_mode = 2
+text = "Cancel"
+
+[connection signal="toggled" from="FlowContainer/mark_this_as_done" to="." method="_on_mark_this_as_done_toggled"]
+[connection signal="pressed" from="FlowContainer/HBoxContainer/confirm" to="." method="_on_confirm_pressed"]
+[connection signal="pressed" from="FlowContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"]
diff --git a/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd b/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd
new file mode 100644
index 0000000..45e480a
--- /dev/null
+++ b/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd
@@ -0,0 +1,107 @@
+class_name Migrator4647
+extends Node
+
+enum MigrationAction {
+ UPGRADE_4_7,
+ DOWNGRADE_4_6,
+ DO_NOTHING
+}
+
+const MIGRATION_DONE = "migration_done"
+const GODOT_4_7_HEX = 0x040700
+
+
+func check_need_upgrade_or_downgrade_scripts(engine_version: Dictionary = Engine.get_version_info()) -> MigrationAction:
+ var data: Dictionary = read_migration_file()
+ var current_version_hex: int = engine_version["hex"]
+ var current_is_4_7_or_newer: bool = current_version_hex >= GODOT_4_7_HEX
+ if data.is_empty(): # No existing file : initiate migration
+ if current_is_4_7_or_newer:
+ return MigrationAction.UPGRADE_4_7
+ return MigrationAction.DOWNGRADE_4_6
+
+ if not data[MIGRATION_DONE]:
+ if data["last_engine_version_hex"] >= GODOT_4_7_HEX:
+ return MigrationAction.UPGRADE_4_7
+ return MigrationAction.DOWNGRADE_4_6
+
+ var last_migration_version = data["last_engine_version_hex"]
+ var last_is_4_7_or_newer: bool = last_migration_version >= GODOT_4_7_HEX
+ if last_is_4_7_or_newer == current_is_4_7_or_newer:
+ return MigrationAction.DO_NOTHING
+ if last_is_4_7_or_newer:
+ return MigrationAction.DOWNGRADE_4_6
+ return MigrationAction.UPGRADE_4_7
+
+
+func prepare_specific_godot_version(target_hex: int) -> void:
+ var dir = DirAccess.open("res://addons/escoria-core/game/core-scripts/")
+ print("Preparing Escoria for Godot version %s" % [target_hex])
+ if dir == null:
+ print("Failed opening folder res://addons/escoria-core/game/core-scripts/")
+ return
+
+ # Backup existing files, just in case they were edited by gamedev (and so, different from
+ # origin).
+ var datetime: String = Time.get_datetime_string_from_system().replace(":", ".")
+ var res: Error = dir.copy(
+ "res://addons/escoria-core/game/core-scripts/esc_item.gd",
+ "res://addons/escoria-core/game/core-scripts/pre-4.7/esc_item.gd.%s.bak" % datetime
+ )
+ if res != OK:
+ print("Failed backup of existing file res://addons/escoria-core/game/core-scripts/esc_item.gd")
+ return
+
+ # Then remove
+ res = dir.remove("res://addons/escoria-core/game/core-scripts/esc_item.gd")
+ if res != OK:
+ print("Failed removing file res://addons/escoria-core/game/core-scripts/esc_item.gd")
+ return
+
+ if target_hex < GODOT_4_7_HEX: # < 4.7.0
+ # Copy 4.6 files
+ res = DirAccess.copy_absolute(
+ "res://addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd",
+ "res://addons/escoria-core/game/core-scripts/esc_item.gd"
+ )
+ if res != OK:
+ print("Failed copying file res://addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.6.gd")
+ return
+ else: # >= 4.7.x
+ # Copy 4.7 files
+ res = DirAccess.copy_absolute(
+ "res://addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd",
+ "res://addons/escoria-core/game/core-scripts/esc_item.gd"
+ )
+ if res != OK:
+ print("Failed copying file res://addons/escoria-core/game/core-scripts/pre-4.7/esc_item.4.7.gd")
+ return
+
+ # Lastly, store the last used engine version in a file
+ save_migration_file(true)
+
+ EditorInterface.restart_editor()
+
+
+func save_migration_file(migration_done: bool):
+ var new_last_version_used_file = FileAccess.open("res://addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json", FileAccess.WRITE)
+ var version = Engine.get_version_info()
+ var data: Dictionary = {
+ "last_engine_version": version.string,
+ "last_engine_version_hex": version.hex,
+ "migration_done": migration_done
+ }
+ new_last_version_used_file.store_string(JSON.stringify(data))
+ new_last_version_used_file.close()
+
+
+func read_migration_file() -> Dictionary:
+ var new_last_version_used_file = FileAccess.open("res://addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json", FileAccess.READ)
+ if new_last_version_used_file == null:
+ return {}
+ var parsed: Variant = JSON.parse_string(new_last_version_used_file.get_as_text())
+ new_last_version_used_file.close()
+ if not (parsed is Dictionary):
+ push_error("Error reading file res://addons/escoria-core/game/core-scripts/pre-4.7/last_version_used.json")
+ return {}
+ return parsed
diff --git a/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd.uid b/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd.uid
new file mode 100644
index 0000000..e2eb44a
--- /dev/null
+++ b/addons/escoria-core/editor/migration_4_6_4_7/migrator_4_6_4_7.gd.uid
@@ -0,0 +1 @@
+uid://rdo2ddvl7csi
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]
diff --git a/addons/escoria-core/game/esc_autoload.gd b/addons/escoria-core/game/esc_autoload.gd
index 72ef6f8..a36b9da 100644
--- a/addons/escoria-core/game/esc_autoload.gd
+++ b/addons/escoria-core/game/esc_autoload.gd
@@ -2,6 +2,14 @@ extends Node
## This is Escoria's singleton script.
## It holds accessors to some utils, such as Escoria's logger.
+## Signal sent when Escoria starts a new game[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+signal new_game_started
+
## Signal sent when Escoria is paused[br]
## [br]
## #### Parameters[br]
@@ -24,7 +32,7 @@ signal resumed
## * DIALOG: Game is playing a dialog
## * WAIT: Game is waiting
## * LOADING: Game is currently loading
-enum GAME_STATE {
+enum GameState {
DEFAULT,
DIALOG,
WAIT,
@@ -149,8 +157,8 @@ var creating_new_game: bool = false
ProjectSettings.get_setting("display/window/size/viewport_width"),
ProjectSettings.get_setting("display/window/size/viewport_height"))
-## Current state of Escoria (GAME_STATE enum)
-@onready var current_state = GAME_STATE.DEFAULT
+## Current state of Escoria (GameState enum)
+@onready var current_state = GameState.DEFAULT
## Ready function. Instantiates the main scene if running a room directly.[br]
## [br]
@@ -196,8 +204,7 @@ func get_escoria():
# We check if we run the full game or a room scene directly
if get_tree().current_scene is ESCMain:
return get_node("/root/main_scene").escoria_node
- else:
- return get_node("main_scene").escoria_node
+ return get_node("main_scene").escoria_node
## Pauses or unpause the game[br]
diff --git a/addons/escoria-core/game/esc_dependency_injector.gd b/addons/escoria-core/game/esc_dependency_injector.gd
index 570ec3d..a80be88 100644
--- a/addons/escoria-core/game/esc_dependency_injector.gd
+++ b/addons/escoria-core/game/esc_dependency_injector.gd
@@ -25,5 +25,3 @@ class_name ESCDependencyInjector
## Returns nothing.
func esc_inventory_item(p_item: ESCItem):
return ESCInventoryItem.new(p_item)
-
-# TODO Add methods for relevant classes. \ No newline at end of file
diff --git a/addons/escoria-core/game/esc_inputs_manager.gd b/addons/escoria-core/game/esc_inputs_manager.gd
index 762da5e..b2d8b5b 100644
--- a/addons/escoria-core/game/esc_inputs_manager.gd
+++ b/addons/escoria-core/game/esc_inputs_manager.gd
@@ -2,8 +2,11 @@
##
## Catches, handles and distributes input events for the game.
## @MANAGER
-extends Resource
class_name ESCInputsManager
+extends Resource
+
+## Emitted when input mode changes.
+signal input_mode_changed(new_mode: int)
## Valid input flags[br]
@@ -27,7 +30,12 @@ const ESC_UI_PRIMARY_ACTION = "esc_ui_primary_action"
## The current input mode
-var input_mode = INPUT_ALL
+var input_mode = INPUT_ALL:
+ get:
+ return input_mode
+ set(new_mode):
+ input_mode = new_mode
+ input_mode_changed.emit(new_mode)
## A LIFO stack of hovered items
var hover_stack: HoverStack
@@ -43,7 +51,7 @@ var hotspot_focused: String = ""
## #### Parameters[br]
##[br]
## - event: The event to process[br]
-## - is_default_state: Whether the current state is escoria.GAME_STATE.DEFAULT[br]
+## - is_default_state: Whether the current state is escoria.GameState.DEFAULT[br]
##[br]
## **Returns** Whether the function processed the event.
var custom_input_handler = null
@@ -77,7 +85,7 @@ func _init():
## #### Returns[br]
## [br]
## Returns nothing.
-func _on_event_finished(return_code: int, event_name: String):
+func _on_event_finished(_return_code: int, _event_name: String):
if _hovered_element == null:
hotspot_focused = ""
@@ -138,7 +146,7 @@ func register_background(background: ESCBackground):
## [br]
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
-## |callback|`Variant`|Function reference satisfying the above contract event The event to process is_default_state Whether the current state is escoria.GAME_STATE.DEFAULT returns whether the function processed the event `callback` is responsible for calling `get_tree().set_input_as_handled()`, if appropriate.|yes|[br]
+## |callback|`Variant`|Function reference satisfying the above contract event The event to process is_default_state Whether the current state is escoria.GameState.DEFAULT returns whether the function processed the event `callback` is responsible for calling `get_tree().set_input_as_handled()`, if appropriate.|yes|[br]
## [br]
## #### Returns[br]
## [br]
@@ -154,7 +162,7 @@ func register_custom_input_handler(callback) -> void:
## | Name | Type | Description | Required? |[br]
## |:-----|:-----|:------------|:----------|[br]
## |event|`InputEvent`|The event to process|yes|[br]
-## |is_default_state|`bool`|Whether the current state is escoria.GAME_STATE.DEFAULT|yes|[br]
+## |is_default_state|`bool`|Whether the current state is escoria.GameState.DEFAULT|yes|[br]
## [br]
## #### Returns[br]
## [br]
@@ -162,8 +170,8 @@ func register_custom_input_handler(callback) -> void:
func try_custom_input_handler(event: InputEvent, is_default_state: bool) -> bool:
if custom_input_handler:
return custom_input_handler.call(event, is_default_state)
- else:
- return false
+
+ return false
## Callback called by hover stack content change.[br]
## [br]
@@ -209,9 +217,9 @@ func set_hovered_node(item: ESCItem) -> bool:
_hovered_element = item
_hovered_element.mouse_entered()
return true
+
# Else, the tested item is currently on top of hover stack, then do nothing
- else:
- return false
+ return false
## Unsets the hovered node.[br]
@@ -506,10 +514,10 @@ func on_item_non_interactive(item: ESCItem) -> void:
if hover_stack.is_empty():
return
- else:
- var new_item = hover_stack.get_top_item()
- escoria.action_manager.set_action_input_state(ESCActionManager.ACTION_INPUT_STATE.AWAITING_VERB_OR_ITEM)
- new_item.mouse_entered()
+
+ var new_item = hover_stack.get_top_item()
+ escoria.action_manager.set_action_input_state(ESCActionManager.ActionInputState.AWAITING_VERB_OR_ITEM)
+ new_item.mouse_entered()
# An Escoria item was clicked with the LMB
diff --git a/addons/escoria-core/game/esc_project_settings_manager.gd b/addons/escoria-core/game/esc_project_settings_manager.gd
index 3a2b10b..492a269 100644
--- a/addons/escoria-core/game/esc_project_settings_manager.gd
+++ b/addons/escoria-core/game/esc_project_settings_manager.gd
@@ -1,7 +1,7 @@
## Registers and allows access to Escoria-specific project settings.
## @MANAGER
-extends Resource
class_name ESCProjectSettingsManager
+extends Resource
## Root for Escoria-specific project settings.
const _ESCORIA_SETTINGS_ROOT = "escoria"
@@ -47,6 +47,9 @@ const SETTINGS_PATH = _ESCORIA_SETTINGS_ROOT + "/" + _MAIN_ROOT + "/" + "setting
const TEXT_LANG = _ESCORIA_SETTINGS_ROOT + "/" + _MAIN_ROOT + "/" + "text_lang"
## Path to the voice language setting.
const VOICE_LANG = _ESCORIA_SETTINGS_ROOT + "/" + _MAIN_ROOT + "/" + "voice_lang"
+## Save sounds playback position in savegames. If this is false, playback will resume from beginning
+## after loading.
+const SAVE_SOUNDS_PLAYBACK_POSITION = _ESCORIA_SETTINGS_ROOT + "/" + _MAIN_ROOT + "/" + "save_sounds_playback_position"
## Debug-related Escoria project settings root.
const _DEBUG_ROOT = "debug"
diff --git a/addons/escoria-core/game/escoria.gd b/addons/escoria-core/game/escoria.gd
index 9da2e9f..1fb5e91 100644
--- a/addons/escoria-core/game/escoria.gd
+++ b/addons/escoria-core/game/escoria.gd
@@ -1,6 +1,6 @@
@tool
-extends Node
class_name Escoria
+extends Node
## The main Escoria script.
@@ -173,7 +173,6 @@ func _input(event: InputEvent):
if event.is_action_pressed("ui_cancel"):
request_pause_menu.emit()
- pass
## Runs the event "event_name" from the "script" ESC script.[br]
@@ -188,19 +187,20 @@ func _input(event: InputEvent):
## [br]
## #### Returns[br]
## [br]
-## Returns Nothing. Waits for the event to finish before returning. (`Variant`)
-func run_event_from_script(script: ESCScript, event_name: String, from_statement_id: int = 0):
+## Returns ESCExecution. Waits for the event to finish before returning. (`Variant`)
+func run_event_from_script(script: ESCScript, event_name: String, _from_statement_id: int = 0):
if script == null:
escoria.logger.error(
self,
"Requested event %s on unloaded script %s." % [event_name, script] +
"Please load the ESC script using esc_compiler.load_esc_file()."
)
+ return ESCExecution.RC_WONT_QUEUE
if not _event_exists_in_script(script, event_name):
- return
+ return ESCExecution.RC_WONT_QUEUE
- escoria.event_manager.queue_event(script.events[event_name])
+ escoria.event_manager.queue_event(script.get_event_with_target(event_name))
var rc = await escoria.event_manager.event_finished
while rc[1] != event_name:
rc = await escoria.event_manager.event_finished
@@ -210,10 +210,12 @@ func run_event_from_script(script: ESCScript, event_name: String, from_statement
self,
"Start event of the start script returned unsuccessful: %d." % rc[0]
)
- return
+
+ return rc[0]
## Checks for the existence of both mandatory and optional events within a specified script.[br]
+## We ignore any target for the event, at least for now.[br]
## [br]
## #### Parameters[br]
## [br]
@@ -226,7 +228,7 @@ func run_event_from_script(script: ESCScript, event_name: String, from_statement
## [br]
## Returns True iff event_name exists within script. Method will terminate execution of the program if the specified event is required and doesn't exist. (`bool`)
func _event_exists_in_script(script: ESCScript, event_name: String) -> bool:
- if script.events.has(event_name):
+ if script.has_event_with_target(event_name):
return true
if event_name in escoria.event_manager.REQUIRED_EVENTS:
@@ -267,7 +269,9 @@ func _event_exists_in_script(script: ESCScript, event_name: String) -> bool:
func new_game():
escoria.event_manager.interrupt() # interrupt first because we're going to reset the interpreter
escoria.game_scene.escoria_show_ui()
+ _reset_new_game_runtime_state()
escoria.globals_manager.clear()
+ escoria.object_manager.reset_game_state()
escoria.interpreter_factory.reset_interpreter()
escoria.main.clear_previous_scene()
escoria.creating_new_game = true
@@ -276,7 +280,24 @@ func new_game():
true,
true
)
- run_event_from_script(escoria.start_script, escoria.event_manager.EVENT_NEW_GAME)
+
+ var rc = await run_event_from_script(escoria.start_script, escoria.event_manager.EVENT_NEW_GAME)
+ if rc == ESCExecution.RC_OK:
+ escoria.new_game_started.emit()
+
+
+func _reset_new_game_runtime_state() -> void:
+ escoria.action_manager.clear_current_action()
+ escoria.action_manager.clear_current_tool()
+ escoria.inputs_manager.hover_stack.clear()
+ escoria.inputs_manager.hotspot_focused = ""
+ escoria.inputs_manager.input_mode = escoria.inputs_manager.INPUT_ALL
+
+ if escoria.game_scene and escoria.game_scene.is_inside_tree():
+ escoria.game_scene.clear_tooltip()
+ escoria.game_scene.close_inventory()
+ escoria.game_scene.hide_main_menu()
+ escoria.game_scene.unpause_game()
## Function called to quit the game.[br]
diff --git a/addons/escoria-core/game/main.gd b/addons/escoria-core/game/main.gd
index e9c6f84..1dd0807 100644
--- a/addons/escoria-core/game/main.gd
+++ b/addons/escoria-core/game/main.gd
@@ -25,6 +25,9 @@ var previous_scene: Node
## The Escoria context currently in wait state
var wait_level
+## Last used camera limit id by room global id
+var camera_limit_id_by_room_global_id: Dictionary = {}
+
## Reference to the scene transition node
@onready var scene_transition: ESCTransitionPlayer
@@ -180,6 +183,98 @@ func _on_wait_finished() -> void:
escoria.esc_level_runner.finished(wait_level)
+## Returns true iff the specified camera limit id can be applied to the scene.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## |camera_limit_id|`int`|The camera limit id to check.|yes|[br]
+## |scene|`Node`|The scene to check against.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns `true` if the specified camera limit id can be applied to the scene.
+func _is_valid_camera_limit_id_for_room(camera_limit_id: int, scene: Node) -> bool:
+ if scene == null or not is_instance_valid(scene):
+ return false
+ if not (scene is ESCRoom):
+ return false
+
+ var last_available_camera_limit = scene.camera_limits.size() - 1
+ return camera_limit_id >= 0 and camera_limit_id <= last_available_camera_limit
+
+
+## Clears stored camera limit id for the specified scene global id.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## |scene_global_id|`String`|The global id of the scene.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func clear_camera_limit_id_for_room(scene_global_id: String) -> void:
+ if scene_global_id == "":
+ return
+ camera_limit_id_by_room_global_id.erase(scene_global_id)
+
+
+## Returns the stored camera limit id for the scene if it is valid.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## |room|`Node`|The room to get the camera limit id for.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns the stored camera limit id for the scene if it is valid.
+func get_camera_limit_id_for_room(room: Node) -> Variant:
+ if room == null or not is_instance_valid(room):
+ return null
+ if not (room is ESCRoom):
+ return null
+ if not camera_limit_id_by_room_global_id.has(room.global_id):
+ return null
+
+ var stored_camera_limit_id = camera_limit_id_by_room_global_id[room.global_id]
+ if typeof(stored_camera_limit_id) != TYPE_INT:
+ camera_limit_id_by_room_global_id.erase(room.global_id)
+ return null
+ if not _is_valid_camera_limit_id_for_room(stored_camera_limit_id, room):
+ camera_limit_id_by_room_global_id.erase(room.global_id)
+ return null
+
+ return stored_camera_limit_id
+
+
+## Restores camera limits for a scene global id iff the id belongs to that scene.
+## [br]
+## #### Parameters[br]
+## [br]
+## |room_global_id|`String`|The room to get the camera limit id for.|yes|[br]
+## |camera_limit_id|`int`|The camera limit id to restore for the room.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func restore_camera_limit_for_room_global_id(room_global_id: String, camera_limit_id: Variant) -> void:
+ if room_global_id == "":
+ return
+ if current_scene == null or not (current_scene is ESCRoom):
+ clear_camera_limit_id_for_room(room_global_id)
+ return
+ if current_scene.global_id != room_global_id:
+ clear_camera_limit_id_for_room(room_global_id)
+ return
+ if typeof(camera_limit_id) != TYPE_INT:
+ clear_camera_limit_id_for_room(room_global_id)
+ return
+ if not _is_valid_camera_limit_id_for_room(camera_limit_id, current_scene):
+ clear_camera_limit_id_for_room(room_global_id)
+ return
+
+ set_camera_limits(camera_limit_id, current_scene)
+
+
## Set the camera limits[br]
## [br]
## #### Parameters[br]
@@ -194,8 +289,13 @@ func _on_wait_finished() -> void:
## Returns nothing.
func set_camera_limits(camera_limit_id: int = 0, scene: Node = current_scene) -> void:
var limits = {}
+ if scene == null or not is_instance_valid(scene):
+ return
+ if not (scene is ESCRoom):
+ return
+
var last_available_camera_limit = scene.camera_limits.size() - 1
- if camera_limit_id > last_available_camera_limit:
+ if camera_limit_id < 0 or camera_limit_id > last_available_camera_limit:
escoria.logger.error(
self,
"Camera3D limit %d requested. Last available camera limit is %d." % [
@@ -203,6 +303,8 @@ func set_camera_limits(camera_limit_id: int = 0, scene: Node = current_scene) ->
last_available_camera_limit
]
)
+ camera_limit_id_by_room_global_id.erase(scene.global_id)
+ return
var scene_camera_limits = scene.camera_limits[camera_limit_id]
if scene_camera_limits.size.x == 0 and scene_camera_limits.size.y == 0:
var area = Rect2()
@@ -248,6 +350,8 @@ func set_camera_limits(camera_limit_id: int = 0, scene: Node = current_scene) ->
escoria.object_manager.get_object(
escoria.object_manager.CAMERA
).node.set_limits(limits)
+ camera_limit_id_by_room_global_id[scene.global_id] = camera_limit_id
+
## Save the game state to the provided savegame resource.[br]
@@ -402,3 +506,19 @@ func show_ui() -> void:
func show_current_scene() -> void:
if escoria.main.current_scene != null:
escoria.main.current_scene.show()
+
+## Stops all sound channels.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func stop_all_sounds() -> void:
+ var stop_snd: StopSndCommand = StopSndCommand.new()
+ stop_snd.run([ESCObjectManager.AMBIENT])
+ stop_snd.run([ESCObjectManager.MUSIC])
+ stop_snd.run([ESCObjectManager.SOUND])
+ stop_snd.run([ESCObjectManager.SPEECH])
diff --git a/addons/escoria-core/game/main_scene.gd b/addons/escoria-core/game/main_scene.gd
index 423c425..6701703 100644
--- a/addons/escoria-core/game/main_scene.gd
+++ b/addons/escoria-core/game/main_scene.gd
@@ -1,8 +1,8 @@
## Main_scene is the entry point for Godot Engine.
##
## This scene sets up the main scene to load.
-extends Node
class_name ESCMain
+extends Node
## Reference to the Escoria node instance.
var escoria_node: Escoria
diff --git a/addons/escoria-core/game/scenes/camera_player/camera.tscn b/addons/escoria-core/game/scenes/camera_player/camera.tscn
index 4dc2e9c..34def51 100644
--- a/addons/escoria-core/game/scenes/camera_player/camera.tscn
+++ b/addons/escoria-core/game/scenes/camera_player/camera.tscn
@@ -1,9 +1,8 @@
-[gd_scene load_steps=2 format=3 uid="uid://dmw5gicuenj53"]
+[gd_scene format=3 uid="uid://dmw5gicuenj53"]
[ext_resource type="Script" uid="uid://b8xyisawuhtw3" path="res://addons/escoria-core/game/scenes/camera_player/esc_camera.gd" id="1"]
-[node name="camera" type="Camera2D"]
-current = true
+[node name="camera" type="Camera2D" unique_id=636237273]
drag_horizontal_enabled = true
drag_vertical_enabled = true
script = ExtResource("1")
diff --git a/addons/escoria-core/game/scenes/camera_player/esc_camera.gd b/addons/escoria-core/game/scenes/camera_player/esc_camera.gd
index 6473121..b97f050 100644
--- a/addons/escoria-core/game/scenes/camera_player/esc_camera.gd
+++ b/addons/escoria-core/game/scenes/camera_player/esc_camera.gd
@@ -1,6 +1,6 @@
## Camera handling for Escoria scenes.
-extends Camera2D
class_name ESCCamera
+extends Camera2D
## Reference to the tween node for animating camera movements.
var _tween: Tween3:
@@ -43,27 +43,6 @@ func _process(_delta):
if is_instance_valid(_follow_target) and not _tween.is_running() and _follow_target.has_moved():
self.global_position = _follow_target.global_position
-## Register this camera with the object manager so it can be used before being made active as part of the current scene tree.[br]
-## [br]
-## #### Parameters[br]
-## [br]
-## | Name | Type | Description | Required? |[br]
-## |:-----|:-----|:------------|:----------|[br]
-## |room|`Variant`|The room with which to register the camera|no|[br]
-## [br]
-## #### Returns[br]
-## [br]
-## Returns nothing.
-func register(room = null):
- escoria.object_manager.register_object(
- ESCObject.new(
- escoria.object_manager.CAMERA,
- self
- ),
- room,
- true
- )
-
## The camera's tween instance.[br]
## [br]
## #### Parameters[br]
diff --git a/addons/escoria-core/game/scenes/camera_player/esc_camera_limits.gd b/addons/escoria-core/game/scenes/camera_player/esc_camera_limits.gd
index 316e392..292896c 100644
--- a/addons/escoria-core/game/scenes/camera_player/esc_camera_limits.gd
+++ b/addons/escoria-core/game/scenes/camera_player/esc_camera_limits.gd
@@ -1,6 +1,6 @@
## Describes a bounding box that limits the camera movement in the scene.
-extends RefCounted
class_name ESCCameraLimits
+extends RefCounted
## The left side of the bounding box.
var limit_left: int = -10000
diff --git a/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd b/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd
index 0a00456..375c967 100644
--- a/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd
+++ b/addons/escoria-core/game/scenes/dialogs/esc_dialog_manager.gd
@@ -1,7 +1,7 @@
## A base class for dialog plugins to work with Escoria
## @MANAGER
-extends Control
class_name ESCDialogManager
+extends Control
## Emitted when the say function has completed showing the text[br]
## [br]
@@ -40,7 +40,7 @@ signal option_chosen(option)
## #### Returns[br]
## [br]
## Returns whether the type is supported or not. (`bool`)
-func has_type(type: String) -> bool:
+func has_type(_type: String) -> bool:
return false
## Checks whether a specific chooser type is supported by the dialog plugin.[br]
@@ -54,7 +54,7 @@ func has_type(type: String) -> bool:
## #### Returns[br]
## [br]
## Returns whether the type is supported or not. (`bool`)
-func has_chooser_type(type: String) -> bool:
+func has_chooser_type(_type: String) -> bool:
return false
## Outputs a text said by the item specified by the global id and emits `say_finished` after finishing displaying the text.[br]
@@ -72,7 +72,7 @@ func has_chooser_type(type: String) -> bool:
## #### Returns[br]
## [br]
## Returns nothing.
-func say(dialog_player: Node, global_id: String, text: String, type: String, key: String):
+func say(_dialog_player: Node, _global_id: String, _text: String, _type: String, _key: String):
pass
## Instructs the dialog manager to preserve the next dialog box used by a `say` command until a call to `disable_preserve_dialog_box` is made. This method should be idempotent, i.e. if called after the first time and prior to `disable_preserve_dialog_box` being called, the result should be the same.[br]
@@ -112,7 +112,7 @@ func disable_preserve_dialog_box() -> void:
## #### Returns[br]
## [br]
## Returns nothing.
-func choose(dialog_player: Node, dialog: ESCDialog, type: String):
+func choose(_dialog_player: Node, _dialog: ESCDialog, _type: String):
pass
## Triggers running the dialogue faster.[br]
diff --git a/addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd b/addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd
index 44e2a3f..69f7127 100644
--- a/addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd
+++ b/addons/escoria-core/game/scenes/dialogs/esc_dialog_options_chooser.gd
@@ -1,6 +1,6 @@
## Base class for all dialog options implementations
-extends Control
class_name ESCDialogOptionsChooser
+extends Control
## Emitted when an option is chosen.[br]
## [br]
diff --git a/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd b/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd
index 70e61e2..3862696 100644
--- a/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd
+++ b/addons/escoria-core/game/scenes/dialogs/esc_dialog_player.gd
@@ -1,6 +1,6 @@
## Escoria dialog player
-extends Control
class_name ESCDialogPlayer
+extends Control
## Emitted when an answer is chosen.[br]
## [br]
@@ -158,9 +158,9 @@ func _determine_say_dialog_manager(type: String) -> void:
ESCProjectSettingsManager.DIALOG_MANAGERS
):
if ResourceLoader.exists(_manager_class):
- var _manager: ESCDialogManager = load(_manager_class).new()
- if _manager.has_type(type):
- dialog_manager = _manager
+ var manager: ESCDialogManager = load(_manager_class).new()
+ if manager.has_type(type):
+ dialog_manager = manager
else:
dialog_manager = null
@@ -191,9 +191,9 @@ func _determine_choose_dialog_manager(type: String) -> void:
ESCProjectSettingsManager.DIALOG_MANAGERS
):
if ResourceLoader.exists(_manager_class):
- var _manager: ESCDialogManager = load(_manager_class).new()
- if _manager.has_chooser_type(type):
- dialog_manager = _manager
+ var manager: ESCDialogManager = load(_manager_class).new()
+ if manager.has_chooser_type(type):
+ dialog_manager = manager
else:
dialog_manager = null
@@ -248,7 +248,8 @@ func _determine_dialog_manager(dialog_type: String, dialog_manager_type: String)
if dialog_type == DIALOG_TYPE_SAY:
_determine_say_dialog_manager(dialog_manager_type)
return _say_dialog_manager
- elif dialog_type == DIALOG_TYPE_CHOOSE:
+
+ if dialog_type == DIALOG_TYPE_CHOOSE:
_determine_choose_dialog_manager(dialog_manager_type)
return _choose_dialog_manager
diff --git a/addons/escoria-core/game/scenes/esc_prompt/esc_prompt_popup.gd b/addons/escoria-core/game/scenes/esc_prompt/esc_prompt_popup.gd
index d538036..b8d92b2 100644
--- a/addons/escoria-core/game/scenes/esc_prompt/esc_prompt_popup.gd
+++ b/addons/escoria-core/game/scenes/esc_prompt/esc_prompt_popup.gd
@@ -1,14 +1,8 @@
## A debug window which can run esc commands
extends Window
-## Reference to the past actions display
-@onready var past_actions = $VBoxContainer/past_actions
-
-## Reference to the command input
-@onready var command = $VBoxContainer/command
-
-## ESC commands kept around for references to their command names.
-var _print: PrintCommand
+## The maximum number of commands to keep in history
+const COMMANDS_HISTORY_LENGTH: int = 20
## History of typed commands
var commands_history: PackedStringArray
@@ -16,8 +10,14 @@ var commands_history: PackedStringArray
## The current index in the command history
var commands_history_current_id: int
-## The maximum number of commands to keep in history
-const COMMANDS_HISTORY_LENGTH: int = 20
+## ESC commands kept around for references to their command names.
+var _print: PrintCommand
+
+## Reference to the past actions display
+@onready var past_actions = $VBoxContainer/past_actions
+
+## Reference to the command input
+@onready var command = $VBoxContainer/command
## Called when the node is added to the scene tree for the first time.[br]
## [br]
@@ -111,7 +111,7 @@ func _on_command_text_entered(p_command_str : String):
if script:
escoria.logger.dont_assert = true
- escoria.event_manager.queue_event(script.events[escoria.event_manager.EVENT_PRINT])
+ escoria.event_manager.queue_event(script.get_event_with_target(escoria.event_manager.EVENT_PRINT))
var ret = await escoria.event_manager.event_finished
while ret[1] != _print.get_command_name():
ret = await escoria.event_manager.event_finished
diff --git a/addons/escoria-core/game/scenes/inventory/inventory_ui.gd b/addons/escoria-core/game/scenes/inventory/inventory_ui.gd
index 082c250..e3e8dcf 100644
--- a/addons/escoria-core/game/scenes/inventory/inventory_ui.gd
+++ b/addons/escoria-core/game/scenes/inventory/inventory_ui.gd
@@ -1,6 +1,6 @@
## Manages the inventory on the GUI connected to the inventory_ui_container variable.
-extends Control
class_name ESCInventory
+extends Control
## The actual container node to add items as children of. Should be a Container.
@export var inventory_ui_container: NodePath
@@ -167,7 +167,7 @@ func remove_item_by_id(item_id: String) -> void:
## #### Returns[br]
## [br]
## Returns nothing.
-func _on_escoria_global_changed(global: String, old_value, new_value) -> void:
+func _on_escoria_global_changed(global: String, _old_value, new_value) -> void:
if !global.begins_with("i/"):
return
var item = global.rsplit("i/", false)
diff --git a/addons/escoria-core/game/scenes/sound/esc_ambient_player.gd b/addons/escoria-core/game/scenes/sound/esc_ambient_player.gd
index 298e7db..1ab7f32 100644
--- a/addons/escoria-core/game/scenes/sound/esc_ambient_player.gd
+++ b/addons/escoria-core/game/scenes/sound/esc_ambient_player.gd
@@ -1,6 +1,6 @@
## Background ambient sound player
-extends Control
class_name ESCAmbientPlayer
+extends Control
## Global id of the background ambient sound player.
@export var global_id: String = "_ambient"
diff --git a/addons/escoria-core/game/scenes/sound/esc_music_player.gd b/addons/escoria-core/game/scenes/sound/esc_music_player.gd
index b8f6c45..5cef646 100644
--- a/addons/escoria-core/game/scenes/sound/esc_music_player.gd
+++ b/addons/escoria-core/game/scenes/sound/esc_music_player.gd
@@ -1,6 +1,6 @@
## Background music player
-extends Control
class_name ESCMusicPlayer
+extends Control
## Global id of the background music player.
@export var global_id: String = "_music"
diff --git a/addons/escoria-core/game/scenes/sound/esc_sound_player.gd b/addons/escoria-core/game/scenes/sound/esc_sound_player.gd
index 2a9fa64..f18abcc 100644
--- a/addons/escoria-core/game/scenes/sound/esc_sound_player.gd
+++ b/addons/escoria-core/game/scenes/sound/esc_sound_player.gd
@@ -1,6 +1,6 @@
## Background sound player
-extends Control
class_name ESCSoundPlayer
+extends Control
## Global id of the sfx sound player.
@export var global_id: String = "_sound"
diff --git a/addons/escoria-core/game/scenes/sound/esc_speech_player.gd b/addons/escoria-core/game/scenes/sound/esc_speech_player.gd
index 31fc668..34c0483 100644
--- a/addons/escoria-core/game/scenes/sound/esc_speech_player.gd
+++ b/addons/escoria-core/game/scenes/sound/esc_speech_player.gd
@@ -1,6 +1,6 @@
## Speech player
-extends Control
class_name ESCSpeechPlayer
+extends Control
## Global id of the speech player.
@export var global_id: String = "_speech"
@@ -21,7 +21,7 @@ class_name ESCSpeechPlayer
## #### Returns[br]
## [br]
## Returns nothing.
-func set_state(p_state: String, from_seconds: float = 0.0, p_force: bool = false) -> void:
+func set_state(p_state: String, from_seconds: float = 0.0, _p_force: bool = false) -> void:
# If speech is disabled, return
if not ESCProjectSettingsManager.get_setting(
ESCProjectSettingsManager.SPEECH_ENABLED
diff --git a/addons/escoria-core/game/scenes/transitions/esc_transition_player.gd b/addons/escoria-core/game/scenes/transitions/esc_transition_player.gd
index 40b0b61..1bf71cf 100644
--- a/addons/escoria-core/game/scenes/transitions/esc_transition_player.gd
+++ b/addons/escoria-core/game/scenes/transitions/esc_transition_player.gd
@@ -1,6 +1,6 @@
## A transition player for scene changes
-extends ColorRect
class_name ESCTransitionPlayer
+extends ColorRect
## Emitted when the transition was played[br]
## [br]
@@ -13,7 +13,7 @@ class_name ESCTransitionPlayer
signal transition_done(transition_id)
## The valid transition modes
-enum TRANSITION_MODE {
+enum TransitionMode {
IN,
OUT
}
@@ -68,7 +68,7 @@ func _ready() -> void:
## Returns the transition id. (`int`)
func transition(
transition_name: String = "",
- mode: int = TRANSITION_MODE.IN,
+ mode: int = TransitionMode.IN,
duration: float = 1.0
) -> int:
@@ -105,7 +105,7 @@ func transition(
var start = 0.0
var end = 1.0
- if mode == TRANSITION_MODE.OUT:
+ if mode == TransitionMode.OUT:
start = 1.0
end = 0.0
diff --git a/addons/escoria-core/game/tween3.gd b/addons/escoria-core/game/tween3.gd
index 88a3e8a..ee8f07a 100644
--- a/addons/escoria-core/game/tween3.gd
+++ b/addons/escoria-core/game/tween3.gd
@@ -1,7 +1,27 @@
## In Godot 4, tweens cannot be resumed. But if you re-create them,[br]
## the bindings are lost. So this class wraps a tween and offers a reset function.
-extends RefCounted
class_name Tween3
+extends RefCounted
+
+
+## Emitted when the tween finishes.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+signal finished()
+
+
+## The parent node for the tween.
+var _tween_parent: Node
+
+## The Tween instance being managed.
+var _tween: Tween
+
+## The duration of the tween.
+var _duration: float
+
## Interpolates a property on an object using a tween.[br]
## [br]
@@ -44,23 +64,6 @@ static func tween_interpolate_property(
return true
-## The parent node for the tween.
-var _tween_parent: Node
-
-## The Tween instance being managed.
-var _tween: Tween
-
-## The duration of the tween.
-var _duration: float
-
-## Emitted when the tween finishes.[br]
-## [br]
-## #### Parameters[br]
-## [br]
-## None.
-## [br]
-signal finished()
-
## Called when the tween finishes.[br]
## [br]
## #### Parameters[br]
@@ -100,7 +103,22 @@ func _init(tween_parent: Node):
## [br]
## Returns nothing.
func _create_tween():
- _tween = _tween_parent.get_tree().create_tween()
+ var tree = _tween_parent.get_tree()
+ if tree == null:
+ escoria.logger.error(
+ self,
+ "Tween3: Parent node is not in the scene tree. Cannot create tween."
+ )
+ return
+
+ _tween = tree.create_tween()
+ if _tween == null:
+ escoria.logger.error(
+ self,
+ "Tween3: Failed to create tween."
+ )
+ return
+
_tween.pause() # prevent autoplay
_tween.finished.connect(_on_finished)
diff --git a/addons/escoria-core/patterns/state_machine/state.gd b/addons/escoria-core/patterns/state_machine/state.gd
index cfa9e0f..0d28ac2 100644
--- a/addons/escoria-core/patterns/state_machine/state.gd
+++ b/addons/escoria-core/patterns/state_machine/state.gd
@@ -1,5 +1,5 @@
-extends Node
class_name State
+extends Node
## Base interface for all states.
##
## This class doesn't do anything in itself but forces us to pass the right
diff --git a/addons/escoria-core/patterns/state_machine/state_machine.gd b/addons/escoria-core/patterns/state_machine/state_machine.gd
index 2fa5d96..95a3e64 100644
--- a/addons/escoria-core/patterns/state_machine/state_machine.gd
+++ b/addons/escoria-core/patterns/state_machine/state_machine.gd
@@ -1,5 +1,5 @@
-extends Node
class_name StateMachine
+extends Node
## Base interface for a generic state machine
## It handles initializing, setting the machine active or not
## delegating _physics_process, _input calls to the State nodes,
@@ -15,9 +15,9 @@ class_name StateMachine
## [br]
signal state_changed(current_state)
-## Starting state. This node has to be set *before* the initialize(START_STATE)
+## Starting state. This node has to be set *before* the initialize(start_state)
## command is called.
-var START_STATE: Node
+var start_state: Node
## List of states
var states_map = {}
@@ -48,13 +48,14 @@ var _active = false:
## [br]
## Returns nothing.
func initialize(start_state: State):
- if START_STATE == null:
+ if start_state == null:
escoria.logger.error(
self,
- "Starting state is required to be initialized with a defined state,
- but it is null. Escoria cannot determine which of the defined states
- (in states_map dictionary) is supposed to be the starting one.
- Please assign a state to START_STATE in your implementation of the StateMachine class.")
+ "Starting state is required to be initialized with a defined state," + \
+ "but it is null. Escoria cannot determine which of the defined states" + \
+ "(in states_map dictionary) is supposed to be the starting one." + \
+ "Please assign a state to start_state in your implementation of the StateMachine class.")
+ return
for child in get_children():
child.connect("finished", Callable(self, "_change_state"))
diff --git a/addons/escoria-core/plugin.gd b/addons/escoria-core/plugin.gd
index 8c5dc09..0fb8dab 100644
--- a/addons/escoria-core/plugin.gd
+++ b/addons/escoria-core/plugin.gd
@@ -2,6 +2,8 @@
## Plugin script to initialize Escoria
extends EditorPlugin
+
+
## Comma separator const used to build enabled extensions.
const COMMA_SEPARATOR = ","
@@ -13,6 +15,7 @@ const ASHES_ANALYZER_MENU_ITEM = "Analyze ASHES Scripts"
## The warning popup displayed on escoria-core enabling.
var popup_info: AcceptDialog
+var popup_migration: AcceptDialogMigrate4647
## ASHES scripts analyzer. Needed to allow calling the analyzer from
## Project>Tools menu.
@@ -33,6 +36,7 @@ func _enable_plugin():
"escoria",
"res://addons/escoria-core/game/esc_autoload.gd"
)
+
# Prepare settings
_set_escoria_main_settings()
_set_escoria_debug_settings()
@@ -62,7 +66,6 @@ func _enable_plugin():
get_editor_interface().get_editor_main_screen().add_child(popup_info)
popup_info.popup_centered()
-
## Callback for warning popup displayed on escoria-core plugin enabling.[br]
## [br]
## #### Parameters[br]
@@ -73,7 +76,7 @@ func _enable_plugin():
## [br]
## Returns nothing.
func _on_warning_popup_confirmed():
- popup_info.queue_free()
+ popup_info.hide()
## Virtual function called when plugin is disabled.[br]
@@ -103,6 +106,13 @@ func _enter_tree():
# have to add this here since reloading the project doesn't re-add the Tools menu item
add_tool_menu_item(ASHES_ANALYZER_MENU_ITEM, _compiler_analyzer.analyze)
+ popup_migration = preload("res://addons/escoria-core/editor/migration_4_6_4_7/acceptdialog_migrate_4_6_4_7.tscn").instantiate()
+ get_editor_interface().get_base_control().add_child(popup_migration)
+ if popup_migration.required_migration:
+ popup_migration.popup_centered()
+ else:
+ get_editor_interface().get_base_control().remove_child(popup_migration)
+
## Called when Escoria plugin gets removed from Godot Editor's tree.[br]
## [br]
@@ -307,6 +317,14 @@ func _set_escoria_main_settings():
}
)
+ register_setting(
+ ESCProjectSettingsManager.SAVE_SOUNDS_PLAYBACK_POSITION,
+ true,
+ {
+ "type": TYPE_BOOL,
+ }
+ )
+
## Prepare the settings in the Escoria debug category[br]
## [br]
diff --git a/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd
new file mode 100644
index 0000000..e03d122
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd
@@ -0,0 +1,1879 @@
+# GdUnit generated TestSuite
+class_name ESCInterpreterTest
+extends GdUnitTestSuite
+
+@warning_ignore("unused_parameter")
+@warning_ignore("return_value_discarded")
+
+const MARK_SCENE := preload("res://game/characters/mark/mark.tscn")
+const SIMPLE_CHOOSER_SCENE := preload("res://addons/escoria-dialog-simple/chooser/simple.tscn")
+
+var _terminate_on_errors_before: bool
+var _command_directories_before: PackedStringArray
+var _dialog_player_before
+
+
+class DialogPlayerDouble extends ESCDialogPlayer:
+ var _choice_indexes: Array = []
+ var _choices_consumed: int = 0
+ var _dialogs_started: Array = []
+
+
+ func _init(choice_indexes: Array = []):
+ _choice_indexes = choice_indexes.duplicate()
+
+
+ func start_dialog_choices(dialog: ESCDialog, _type: String = "simple"):
+ var option_keys: Array = []
+ var option_texts: Array = []
+ var option_source_indexes: Array = []
+ for option in dialog.options:
+ option_keys.append(option.translation_key)
+ option_texts.append(option.option)
+ option_source_indexes.append(option.source_option_index)
+
+ var timeout_option := dialog.get_timeout_option()
+
+ _dialogs_started.append({
+ "avatar": dialog.avatar,
+ "timeout": dialog.timeout,
+ "timeout_option": dialog.timeout_option,
+ "authored_options_count": dialog.authored_options_count,
+ "visible_timeout_option": timeout_option.option if timeout_option else null,
+ "options_size": dialog.options.size(),
+ "option_keys": option_keys,
+ "option_texts": option_texts,
+ "option_source_indexes": option_source_indexes
+ })
+
+ # Mirror the real dialog player closely enough for interpreter tests:
+ # only valid options are choosable, and scripted indexes deterministically
+ # drive nested dialog flows without needing UI interaction.
+ var valid_options: Array = []
+ for option in dialog.options:
+ if option.is_valid():
+ valid_options.append(option)
+
+ var choice_index := 0
+ if not _choice_indexes.is_empty():
+ choice_index = int(_choice_indexes.pop_front())
+ _choices_consumed += 1
+
+ call_deferred("_emit_choice", valid_options[choice_index] if choice_index < valid_options.size() else null)
+
+
+ func _emit_choice(option) -> void:
+ option_chosen.emit(option)
+
+
+ func get_choices_consumed() -> int:
+ return _choices_consumed
+
+
+ func get_remaining_choices() -> int:
+ return _choice_indexes.size()
+
+
+ func get_dialogs_started() -> Array:
+ return _dialogs_started.duplicate(true)
+
+
+func before() -> void:
+ # Interpreter tests intentionally trigger runtime errors in a few cases, so
+ # they disable terminate-on-error and install a test-only command directory.
+ # The dialog player is also replaced on demand by a deterministic test
+ # double so dialog execution can run headlessly.
+ ESCInterpreterFactory.reset_interpreter()
+ escoria.event_manager.interrupt([], false)
+ escoria.event_manager.clear_event_queue()
+ _terminate_on_errors_before = ESCProjectSettingsManager.get_setting(
+ ESCProjectSettingsManager.TERMINATE_ON_ERRORS
+ )
+ _command_directories_before = PackedStringArray(
+ ESCProjectSettingsManager.get_setting(
+ ESCProjectSettingsManager.COMMAND_DIRECTORIES
+ )
+ )
+ _dialog_player_before = escoria.get("dialog_player")
+
+ ESCProjectSettingsManager.set_setting(
+ ESCProjectSettingsManager.TERMINATE_ON_ERRORS,
+ false
+ )
+ ESCProjectSettingsManager.set_setting(
+ ESCProjectSettingsManager.COMMAND_DIRECTORIES,
+ PackedStringArray([
+ "res://addons/escoria-core/testing/ashes/interpreter/support/commands"
+ ] + Array(_command_directories_before))
+ )
+ escoria.command_registry.registry.clear()
+ escoria.set("dialog_player", null)
+
+
+func after() -> void:
+ # Restore shared Escoria settings and globals so each test starts from the
+ # same baseline and does not leak command registry or dialog-player state.
+ ESCInterpreterFactory.reset_interpreter()
+ escoria.event_manager.interrupt([], false)
+ escoria.event_manager.clear_event_queue()
+ ESCProjectSettingsManager.set_setting(
+ ESCProjectSettingsManager.TERMINATE_ON_ERRORS,
+ _terminate_on_errors_before
+ )
+ ESCProjectSettingsManager.set_setting(
+ ESCProjectSettingsManager.COMMAND_DIRECTORIES,
+ _command_directories_before
+ )
+ escoria.command_registry.registry.clear()
+ escoria.set("dialog_player", _dialog_player_before)
+
+
+func test_execute_block_restores_scope_after_break() -> void:
+ # This fixture creates a nested block-local `x`, exits that block with
+ # `break`, and then reads `x` afterward into a global. If `_execute_block()`
+ # fails to restore `_environment` on the early return, the later lookup will
+ # incorrectly resolve the inner `x` instead of the outer one.
+ var source := _load_fixture("block_scope_restores_after_break.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("block_scope_restores_after_break.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new([], {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ await interpreter.interpret(statements)
+
+ var globals := interpreter.get_global_values()
+ assert_bool(globals.has("result")).is_true()
+ assert_float(float(globals["result"])).is_equal(1.0)
+
+ interpreter.cleanup()
+
+
+func test_invalid_numeric_comparison_does_not_succeed_silently() -> void:
+ # This fixture performs an invalid numeric comparison. Invalid numeric
+ # operations should log an interpreter error and evaluate to `null` rather
+ # than continuing into a native GDScript operator error.
+ var source := _load_fixture("invalid_numeric_comparison.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("invalid_numeric_comparison.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new([], {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ await interpreter.interpret(statements)
+
+ var globals := interpreter.get_global_values()
+ assert_bool(globals.has("result")).is_true()
+ assert_bool(globals["result"] == null).is_true()
+
+ interpreter.cleanup()
+
+
+func test_print_with_zero_arguments_does_not_native_crash() -> void:
+ # This fixture calls the builtin `print()` with zero arguments. The current
+ # implementation only rejects calls with more than one argument, so this
+ # test captures the actual runtime behavior before we decide on a fix.
+ var source := _load_fixture("print_zero_args.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("print_zero_args.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new([], {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ await interpreter.interpret(statements)
+
+ interpreter.cleanup()
+
+
+func test_print_with_two_arguments_does_not_native_crash() -> void:
+ # This fixture calls the builtin `print()` with too many arguments. The
+ # builtin should reject the call cleanly and avoid any native runtime error.
+ var source := _load_fixture("print_two_args.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("print_two_args.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new([], {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ await interpreter.interpret(statements)
+
+ interpreter.cleanup()
+
+
+func test_stop_in_nested_block_prevents_later_statements() -> void:
+ # `stop` should unwind the current event immediately, even when it is issued
+ # from inside a nested block. The later assignment must therefore never run.
+ var outcome := await _interpret_fixture("stop_nested_block.esc")
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_float(float(globals["result"])).is_equal(0.0)
+
+
+func test_done_in_nested_dialog_block_prevents_later_statements() -> void:
+ # `done` should conclude the current dialog from inside a nested block, but
+ # it must not abort the rest of the event. The assignment after the dialog
+ # should still execute, proving the dialog exits cleanly without leaking a
+ # control-flow sentinel to the enclosing event body.
+ var outcome := await _interpret_fixture(
+ "done_nested_block_in_dialog.esc",
+ [0]
+ )
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_float(float(globals["result"])).is_equal(1.0)
+
+
+func test_break_one_returns_to_parent_dialog_level() -> void:
+ # `break 1` should move dialog control back up one level, not continue the
+ # remainder of the current outer option body. The scripted choice sequence
+ # must therefore consume a third outer-dialog choice after the nested break;
+ # if the dialog ends early instead, the final string alone is ambiguous.
+ var outcome := await _interpret_fixture(
+ "nested_dialog_break_one.esc",
+ [0, 0, 1]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("start-done")
+ assert_int(int(outcome.dialog_choices_consumed)).is_equal(3)
+ assert_int(int(outcome.remaining_dialog_choices)).is_equal(0)
+
+
+func test_done_exits_entire_nested_dialog_tree() -> void:
+ # `done` should conclude the entire dialog from any nesting depth. After the
+ # nested choice issues `done`, the outer option body must not continue and
+ # the outer dialog must not be shown again. Execution should resume only
+ # after the dialog statement, so the final value should reflect the initial
+ # value plus the post-dialog append.
+ var outcome := await _interpret_fixture(
+ "done_exits_entire_nested_dialog.esc",
+ [0, 0]
+ )
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("start-done")
+
+
+func test_dialog_followup_runs_after_all_options_are_exhausted() -> void:
+ # When each option invalidates itself, the dialog should conclude once no
+ # valid options remain. Execution must then continue with the statement after
+ # the dialog block.
+ var outcome := await _interpret_fixture(
+ "dialog_exhausted_options_runs_followup.esc",
+ [0, 0]
+ )
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("start-say1-say2-finished")
+ assert_bool(bool(globals["option1_is_valid"])).is_false()
+ assert_bool(bool(globals["option2_is_valid"])).is_false()
+ assert_int(int(outcome.dialog_choices_consumed)).is_equal(2)
+ assert_int(int(outcome.remaining_dialog_choices)).is_equal(0)
+
+
+func test_break_two_exits_two_dialog_levels() -> void:
+ # `break 2` from the innermost dialog should move back up two dialog levels
+ # to the outermost dialog in this fixture shape. As with `break 1`, the test
+ # also verifies that the final scripted outer choice was actually consumed,
+ # which proves the outer dialog stayed alive after the nested unwind.
+ var outcome := await _interpret_fixture(
+ "break_two_exits_two_dialog_levels.esc",
+ [0, 0, 0, 1]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("start-done")
+ assert_int(int(outcome.dialog_choices_consumed)).is_equal(4)
+ assert_int(int(outcome.remaining_dialog_choices)).is_equal(0)
+
+
+func test_stop_in_dialog_option_prevents_later_event_statements() -> void:
+ # `stop` issued from a dialog option should abort the entire event, not just
+ # the current dialog. The assignment after the dialog must therefore never run.
+ var outcome := await _interpret_fixture(
+ "stop_in_dialog_option.esc",
+ [0]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_float(float(outcome.globals["result"])).is_equal(0.0)
+
+
+func test_delayed_command_in_dialog_option_preserves_ordering() -> void:
+ # A blocking command inside a dialog option must complete before the option
+ # body resumes and before execution continues after the dialog. If it does
+ # not block correctly, the final append would observe `unset` instead of the
+ # command-written value.
+ var outcome := await _interpret_fixture(
+ "delayed_command_in_dialog_option.esc",
+ [0]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("cmd-after")
+
+
+func test_delayed_command_in_loop_preserves_ordering() -> void:
+ # A blocking command inside a loop body must finish before the loop can
+ # continue to the next statement. The later append therefore proves the
+ # delayed command completed before the event resumed after the loop.
+ var outcome := await _interpret_fixture("delayed_command_in_loop.esc")
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("cmd-after")
+ assert_bool(outcome.globals.has("count")).is_true()
+ assert_float(float(outcome.globals["count"])).is_equal(1.0)
+
+
+func test_interrupting_delayed_command_stops_event_progress() -> void:
+ # Interrupting an in-flight command should stop the surrounding event from
+ # resuming normal execution. The command records that it observed the
+ # interrupt, and the later append must never run.
+ var source := _load_fixture("delayed_command_interrupt.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("delayed_command_interrupt.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ var event_stmt: ESCGrammarStmts.Event = statements[0]
+ interpreter.call_deferred("interpret", statements)
+ await escoria.get_tree().create_timer(0.05).timeout
+ interpreter.interrupt()
+ await event_stmt.finished
+
+ var globals := interpreter.get_global_values()
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("start")
+ assert_bool(globals.has("was_interrupted")).is_true()
+ assert_bool(bool(globals["was_interrupted"])).is_true()
+ assert_bool(event_stmt.is_interrupted()).is_true()
+
+ interpreter.cleanup()
+
+
+func test_interrupting_delayed_command_in_dialog_stops_event_progress() -> void:
+ # Interrupting an in-flight command inside a dialog option should abort the
+ # surrounding event just as cleanly as interruption in a plain block. The
+ # dialog must not resume and the post-dialog append must never run.
+ var source := _load_fixture("delayed_command_interrupt_in_dialog.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("delayed_command_interrupt_in_dialog.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ var event_stmt: ESCGrammarStmts.Event = statements[0]
+ interpreter.call_deferred("interpret", statements)
+ await escoria.get_tree().create_timer(0.05).timeout
+ interpreter.interrupt()
+ await event_stmt.finished
+
+ var globals := interpreter.get_global_values()
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("start")
+ assert_bool(globals.has("was_interrupted")).is_true()
+ assert_bool(bool(globals["was_interrupted"])).is_true()
+ assert_bool(event_stmt.is_interrupted()).is_true()
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_interrupting_delayed_command_stops_multi_iteration_loop() -> void:
+ # Interrupting a delayed command in the first loop iteration should stop the
+ # loop itself, not just the current statement. The counter must therefore
+ # stay at zero and the post-loop append must never run.
+ var source := _load_fixture("delayed_command_interrupt_in_multi_loop.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("delayed_command_interrupt_in_multi_loop.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ var event_stmt: ESCGrammarStmts.Event = statements[0]
+ interpreter.call_deferred("interpret", statements)
+ await escoria.get_tree().create_timer(0.05).timeout
+ interpreter.interrupt()
+ await event_stmt.finished
+
+ var globals := interpreter.get_global_values()
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("start")
+ assert_bool(globals.has("was_interrupted")).is_true()
+ assert_bool(bool(globals["was_interrupted"])).is_true()
+ assert_bool(globals.has("count")).is_true()
+ assert_float(float(globals["count"])).is_equal(0.0)
+ assert_bool(event_stmt.is_interrupted()).is_true()
+
+ interpreter.cleanup()
+
+
+func test_invalid_command_does_not_continue_event() -> void:
+ # A missing command should fail the current event cleanly instead of letting
+ # later statements continue as if the command had succeeded.
+ var outcome := await _interpret_fixture("invalid_command_does_not_continue.esc")
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("start")
+
+
+func test_invalid_command_in_dialog_does_not_continue_event() -> void:
+ # A missing command inside a dialog option should still terminate the event
+ # cleanly. The assignment after the dialog must not run.
+ var outcome := await _interpret_fixture(
+ "invalid_command_in_dialog_stops_event.esc",
+ [0]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("start")
+
+
+func test_invalid_command_in_loop_does_not_continue_event() -> void:
+ # A missing command inside a loop body should escape the loop as a terminal
+ # runtime error instead of allowing later loop or post-loop statements to run.
+ var outcome := await _interpret_fixture("invalid_command_in_loop_stops_event.esc")
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("start")
+ assert_bool(outcome.globals.has("count")).is_true()
+ assert_float(float(outcome.globals["count"])).is_equal(0.0)
+
+
+func test_script_preserves_multi_target_use_events_without_creating_untargeted_use() -> void:
+ # Room05-style scripts can define the same action multiple times with
+ # different targets. Both targeted events must survive compilation, while an
+ # untargeted lookup for the same action must not accidentally match either.
+ var script_object := _compile_fixture_script("multi_target_use_events.esc")
+
+ assert_bool(script_object.has_event_with_target("use", "r5_wrench")).is_true()
+ assert_bool(script_object.has_event_with_target("use", "r5_pen")).is_true()
+ assert_bool(script_object.has_event_with_target("use")).is_false()
+
+ var wrench_event = script_object.get_event_with_target("use", "r5_wrench")
+ var pen_event = script_object.get_event_with_target("use", "r5_pen")
+
+ assert_object(wrench_event).is_not_null()
+ assert_object(pen_event).is_not_null()
+ assert_object(wrench_event).is_not_same(pen_event)
+ assert_str(wrench_event.get_target_name()).is_equal("r5_wrench")
+ assert_str(pen_event.get_target_name()).is_equal("r5_pen")
+ assert_int(wrench_event.get_num_statements_in_block()).is_equal(1)
+ assert_int(pen_event.get_num_statements_in_block()).is_equal(1)
+
+
+func test_dialog_option_scope_shadowing_restores_outer_local() -> void:
+ # A dialog option body introduces nested block scope. Assignments inside the
+ # option should see the inner shadowing variable, while execution after the
+ # dialog must still resolve the original outer local.
+ var outcome := await _interpret_fixture(
+ "dialog_scope_shadowing.esc",
+ [0]
+ )
+ assert_bool(outcome.globals.has("result")).is_true()
+ assert_str(String(outcome.globals["result"])).is_equal("inner:outer")
+
+
+func test_interrupting_background_channel_does_not_interrupt_front_channel() -> void:
+ # Background-channel interruption should only affect commands running on
+ # that channel. The front event must still complete normally while the
+ # background event should finish as interrupted once its in-flight command
+ # observes the interrupt.
+ var events = _load_fixture_events("channel_interrupt_isolation.esc")
+ assert_bool(events.has_event_with_target("front_event")).is_true()
+ assert_bool(events.has_event_with_target("background_event")).is_true()
+ escoria.globals_manager.set_global("channel_front_result", "start")
+ escoria.globals_manager.set_global("channel_background_result", "start")
+ escoria.globals_manager.set_global("channel_front_was_interrupted", false)
+ escoria.globals_manager.set_global("channel_background_was_interrupted", false)
+
+ var signal_results := {
+ "front": null,
+ "background": null,
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ if event_name == "front_event":
+ signal_results["front"] = [return_code, event_name]
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if event_name == "background_event" and finished_channel_name == "bg_test":
+ signal_results["background"] = [return_code, event_name, finished_channel_name]
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_event"))
+ escoria.event_manager.queue_background_event(
+ "bg_test",
+ events.get_event_with_target("background_event")
+ )
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt_channel("bg_test")
+
+ while signal_results["front"] == null or signal_results["background"] == null:
+ await escoria.get_tree().process_frame
+
+ var front_finished = signal_results["front"]
+ var background_finished = signal_results["background"]
+
+ assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK)
+ assert_int(int(background_finished[0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(escoria.globals_manager.get_global("channel_front_result"))).is_equal("front-cmd-after")
+ assert_bool(escoria.globals_manager.get_global("channel_front_was_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("channel_background_result"))).is_equal("start")
+ assert_bool(escoria.globals_manager.get_global("channel_background_was_interrupted") == true).is_true()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_concurrent_channels_complete_without_interrupting_each_other() -> void:
+ # Two events on different channels should be able to run at the same time
+ # and still complete normally when no interrupt is issued.
+ var events = _load_fixture_events("channel_interrupt_isolation.esc")
+ assert_bool(events.has_event_with_target("front_event")).is_true()
+ assert_bool(events.has_event_with_target("background_event")).is_true()
+ escoria.globals_manager.set_global("channel_front_result", "start")
+ escoria.globals_manager.set_global("channel_background_result", "start")
+ escoria.globals_manager.set_global("channel_front_was_interrupted", false)
+ escoria.globals_manager.set_global("channel_background_was_interrupted", false)
+
+ var signal_results := {
+ "front": null,
+ "background": null,
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ if event_name == "front_event":
+ signal_results["front"] = [return_code, event_name]
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if event_name == "background_event" and finished_channel_name == "bg_test":
+ signal_results["background"] = [return_code, event_name, finished_channel_name]
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_event"))
+ escoria.event_manager.queue_background_event(
+ "bg_test",
+ events.get_event_with_target("background_event")
+ )
+
+ while signal_results["front"] == null or signal_results["background"] == null:
+ await escoria.get_tree().process_frame
+
+ var front_finished = signal_results["front"]
+ var background_finished = signal_results["background"]
+
+ assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK)
+ assert_int(int(background_finished[0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(escoria.globals_manager.get_global("channel_front_result"))).is_equal("front-cmd-after")
+ assert_bool(escoria.globals_manager.get_global("channel_front_was_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("channel_background_result"))).is_equal("bg-cmd-after")
+ assert_bool(escoria.globals_manager.get_global("channel_background_was_interrupted") == true).is_false()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_interrupting_channel_clears_queued_successor_events() -> void:
+ # Interrupting a channel should stop the currently running event and purge
+ # later queued events on that same channel before they ever start running.
+ var events = _load_fixture_events("channel_interrupt_clears_queue.esc")
+ assert_bool(events.has_event_with_target("first")).is_true()
+ assert_bool(events.has_event_with_target("second")).is_true()
+ escoria.globals_manager.set_global("bg_queue_interrupted", false)
+
+ var background_finishes: Array = []
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ background_finishes.append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("first"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("second"))
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt_channel("bg_test")
+
+ while background_finishes.is_empty():
+ await escoria.get_tree().process_frame
+
+ assert_int(background_finishes.size()).is_equal(1)
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(background_finishes[0][1])).is_equal("first")
+ assert_str(String(background_finishes[0][2])).is_equal("bg_test")
+ assert_str(String(escoria.globals_manager.get_global("bg_queue_result"))).is_equal("first")
+ assert_bool(escoria.globals_manager.get_global("bg_queue_interrupted") == true).is_true()
+
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_concurrent_channels_do_not_block_other_channel_queues() -> void:
+ # Each channel should serialize only its own queue. A front-channel event
+ # must complete independently while a background channel works through two
+ # queued events in order.
+ var events = _load_fixture_events("channel_queue_progress_isolated.esc")
+ assert_bool(events.has_event_with_target("front_event")).is_true()
+ assert_bool(events.has_event_with_target("background_first")).is_true()
+ assert_bool(events.has_event_with_target("background_second")).is_true()
+ escoria.globals_manager.set_global("isolated_front_result", "start")
+ escoria.globals_manager.set_global("isolated_background_result", "start")
+ escoria.globals_manager.set_global("isolated_front_interrupted", false)
+ escoria.globals_manager.set_global("isolated_background_interrupted", false)
+
+ var signal_results := {
+ "front": null,
+ "background_finishes": [],
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ if event_name == "front_event":
+ signal_results["front"] = [return_code, event_name]
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ signal_results["background_finishes"].append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_event"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_first"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_second"))
+
+ while signal_results["front"] == null or signal_results["background_finishes"].size() < 2:
+ await escoria.get_tree().process_frame
+
+ var front_finished = signal_results["front"]
+ var background_finishes: Array = signal_results["background_finishes"]
+
+ assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK)
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[0][1])).is_equal("background_first")
+ assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[1][1])).is_equal("background_second")
+ assert_str(String(escoria.globals_manager.get_global("isolated_front_result"))).is_equal("front-cmd-after")
+ assert_bool(escoria.globals_manager.get_global("isolated_front_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("isolated_background_result"))).is_equal("first-cmd-after-first-second")
+ assert_bool(escoria.globals_manager.get_global("isolated_background_interrupted") == true).is_false()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_global_interrupt_stops_running_channels_and_clears_queues() -> void:
+ # A global interrupt should stop every currently running channel and purge
+ # any queued successor events before they start. This fixture queues one
+ # running and one queued event on both `_front` and `bg_test`.
+ var events = _load_fixture_events("global_interrupt_clears_all_channels.esc")
+ assert_bool(events.has_event_with_target("front_running")).is_true()
+ assert_bool(events.has_event_with_target("front_queued")).is_true()
+ assert_bool(events.has_event_with_target("background_running")).is_true()
+ assert_bool(events.has_event_with_target("background_queued")).is_true()
+ escoria.globals_manager.set_global("global_interrupt_front_result", "start")
+ escoria.globals_manager.set_global("global_interrupt_background_result", "start")
+ escoria.globals_manager.set_global("global_interrupt_front_was_interrupted", false)
+ escoria.globals_manager.set_global("global_interrupt_background_was_interrupted", false)
+
+ var signal_results := {
+ "front_finishes": [],
+ "background_finishes": [],
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ signal_results["front_finishes"].append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ signal_results["background_finishes"].append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_running"))
+ escoria.event_manager.queue_event(events.get_event_with_target("front_queued"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_queued"))
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt()
+
+ while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].is_empty():
+ await escoria.get_tree().process_frame
+
+ await escoria.get_tree().create_timer(0.05).timeout
+
+ assert_int(signal_results["front_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_running")
+ assert_int(signal_results["background_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running")
+ assert_str(String(signal_results["background_finishes"][0][2])).is_equal("bg_test")
+ assert_str(String(escoria.globals_manager.get_global("global_interrupt_front_result"))).is_equal("start")
+ assert_bool(escoria.globals_manager.get_global("global_interrupt_front_was_interrupted") == true).is_true()
+ assert_str(String(escoria.globals_manager.get_global("global_interrupt_background_result"))).is_equal("start")
+ assert_bool(escoria.globals_manager.get_global("global_interrupt_background_was_interrupted") == true).is_true()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_queue_event_block_waits_for_correct_background_event_under_concurrency() -> void:
+ # Blocking queue_event waits must ignore unrelated background completions,
+ # including earlier events on the same channel, and return only when the
+ # specifically requested background event finishes.
+ var script_object := _compile_fixture_script("queue_event_block_waits_for_correct_background_event.esc")
+ assert_bool(script_object.has_event_with_target("background_first")).is_true()
+ assert_bool(script_object.has_event_with_target("background_target")).is_true()
+ assert_bool(script_object.has_event_with_target("front_event")).is_true()
+ escoria.globals_manager.set_global("block_wait_result", "start")
+ escoria.globals_manager.set_global("block_wait_front_result", "start")
+
+ escoria.event_manager.queue_background_event(
+ "bg_test",
+ script_object.get_event_with_target("background_first")
+ )
+ escoria.event_manager.queue_event(script_object.get_event_with_target("front_event"))
+
+ var block_rc: int = await escoria.event_manager.queue_event_from_esc(
+ script_object,
+ "background_target",
+ "bg_test",
+ true
+ )
+
+ assert_int(block_rc).is_equal(ESCExecution.RC_OK)
+ assert_str(String(escoria.globals_manager.get_global("block_wait_result"))).is_equal("first-target")
+ assert_str(String(escoria.globals_manager.get_global("block_wait_front_result"))).is_equal("front")
+
+
+func test_queue_event_block_waits_for_correct_front_event_under_concurrency() -> void:
+ # Blocking front queue_event waits must ignore both background completions
+ # and earlier foreground events, returning only when the specifically
+ # requested front event finishes.
+ var script_object := _compile_fixture_script("queue_event_block_waits_for_correct_front_event.esc")
+ assert_bool(script_object.has_event_with_target("front_first")).is_true()
+ assert_bool(script_object.has_event_with_target("front_target")).is_true()
+ assert_bool(script_object.has_event_with_target("background_event")).is_true()
+ escoria.globals_manager.set_global("front_block_wait_result", "start")
+ escoria.globals_manager.set_global("front_block_wait_background_result", "start")
+
+ escoria.event_manager.queue_event(script_object.get_event_with_target("front_first"))
+ escoria.event_manager.queue_background_event(
+ "bg_test",
+ script_object.get_event_with_target("background_event")
+ )
+
+ var block_rc: int = await escoria.event_manager.queue_event_from_esc(
+ script_object,
+ "front_target",
+ "_front",
+ true
+ )
+
+ assert_int(block_rc).is_equal(ESCExecution.RC_OK)
+ assert_str(String(escoria.globals_manager.get_global("front_block_wait_result"))).is_equal("first-target")
+ assert_str(String(escoria.globals_manager.get_global("front_block_wait_background_result"))).is_equal("background")
+
+
+func test_sequential_front_events_preserve_globals_between_runtime_interpreters() -> void:
+ # Each event-manager run currently observes globals written by earlier runs.
+ # The ownership refactor must preserve that user-visible behavior even though
+ # the underlying interpreter instances will stop sharing a singleton slot.
+ var events = _load_fixture_events("interpreter_runtime_state_persists_between_events.esc")
+ assert_bool(events.has_event_with_target("first")).is_true()
+ assert_bool(events.has_event_with_target("second")).is_true()
+
+ var front_finishes: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ front_finishes.append([return_code, event_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("first"))
+ while front_finishes.size() < 1:
+ await escoria.get_tree().process_frame
+
+ escoria.event_manager.queue_event(events.get_event_with_target("second"))
+ while front_finishes.size() < 2:
+ await escoria.get_tree().process_frame
+
+ assert_int(front_finishes.size()).is_equal(2)
+ assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(front_finishes[0][1])).is_equal("first")
+ assert_int(int(front_finishes[1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(front_finishes[1][1])).is_equal("second")
+ assert_str(String(escoria.globals_manager.get_global("interpreter_runtime_result"))).is_equal("first-second")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+
+
+func test_background_event_sees_globals_written_by_previous_front_event() -> void:
+ # Front and background channels each create their own runtime interpreter.
+ # A background event queued after a front event must still observe globals
+ # written by that earlier front-channel execution.
+ var events = _load_fixture_events("interpreter_runtime_state_persists_between_events.esc")
+ assert_bool(events.has_event_with_target("background_seed")).is_true()
+ assert_bool(events.has_event_with_target("background_after")).is_true()
+
+ var front_finishes: Array = []
+ var background_finishes: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ front_finishes.append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ background_finishes.append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("background_seed"))
+ while front_finishes.size() < 1:
+ await escoria.get_tree().process_frame
+
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_after"))
+ while background_finishes.size() < 1:
+ await escoria.get_tree().process_frame
+
+ assert_int(front_finishes.size()).is_equal(1)
+ assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(front_finishes[0][1])).is_equal("background_seed")
+ assert_int(background_finishes.size()).is_equal(1)
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[0][1])).is_equal("background_after")
+ assert_str(String(background_finishes[0][2])).is_equal("bg_test")
+ assert_str(String(escoria.globals_manager.get_global("interpreter_runtime_background_result"))).is_equal("first-background")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_running_interpreter_sees_new_global_defined_by_other_channel() -> void:
+ # A runtime interpreter that is already running must observe globals declared
+ # by a concurrent channel after it has started. Otherwise overlapping events
+ # can regress from shared-global behavior to "Undefined variable" failures.
+ var events = _load_fixture_events("concurrent_new_global_visibility.esc")
+ assert_bool(events.has_event_with_target("front_waits_for_new_global")).is_true()
+ assert_bool(events.has_event_with_target("background_declares_new_global")).is_true()
+
+ var front_finishes: Array = []
+ var background_finishes: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ front_finishes.append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ background_finishes.append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_waits_for_new_global"))
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_declares_new_global"))
+
+ while front_finishes.is_empty() or background_finishes.is_empty():
+ await escoria.get_tree().process_frame
+
+ assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(str(front_finishes[0][1])).is_equal("front_waits_for_new_global")
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(str(background_finishes[0][1])).is_equal("background_declares_new_global")
+ assert_str(str(escoria.globals_manager.get_global("concurrent_visibility_result"))).is_equal("from-background")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_queue_background_event_allows_consecutive_duplicate_queue_entries() -> void:
+ # Queueing the same background event twice in a row should be allowed on a
+ # background channel. In practice these queues are populated via ASHES
+ # commands, so the second request should remain queued behind the first run.
+ var events = _load_fixture_events("background_duplicate_suppression.esc")
+ assert_bool(events.has_event_with_target("repeat_event")).is_true()
+ escoria.globals_manager.set_global("background_duplicate_count", 0)
+ escoria.globals_manager.set_global("background_duplicate_last", "start")
+
+ var background_finishes: Array = []
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ background_finishes.append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event"))
+
+ while background_finishes.size() < 2:
+ await escoria.get_tree().process_frame
+
+ assert_int(background_finishes.size()).is_equal(2)
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[0][1])).is_equal("repeat_event")
+ assert_str(String(background_finishes[0][2])).is_equal("bg_test")
+ assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[1][1])).is_equal("repeat_event")
+ assert_str(String(background_finishes[1][2])).is_equal("bg_test")
+ assert_int(int(escoria.globals_manager.get_global("background_duplicate_count"))).is_equal(2)
+ assert_str(String(escoria.globals_manager.get_global("background_duplicate_last"))).is_equal("tick")
+
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_queue_background_event_allows_same_event_after_current_run_starts() -> void:
+ # Once the first background event is already running, queueing the same event
+ # again on that channel should queue a second run behind it instead of being
+ # suppressed as a duplicate.
+ var events = _load_fixture_events("background_duplicate_suppression.esc")
+ assert_bool(events.has_event_with_target("repeat_event")).is_true()
+ escoria.globals_manager.set_global("background_duplicate_count", 0)
+ escoria.globals_manager.set_global("background_duplicate_last", "start")
+
+ var background_finishes: Array = []
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ background_finishes.append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event"))
+ await escoria.get_tree().create_timer(0.01).timeout
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event"))
+
+ while background_finishes.size() < 2:
+ await escoria.get_tree().process_frame
+
+ assert_int(background_finishes.size()).is_equal(2)
+ assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[0][1])).is_equal("repeat_event")
+ assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(background_finishes[1][1])).is_equal("repeat_event")
+ assert_int(int(escoria.globals_manager.get_global("background_duplicate_count"))).is_equal(2)
+ assert_str(String(escoria.globals_manager.get_global("background_duplicate_last"))).is_equal("tick")
+
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_global_interrupt_preserves_exception_events() -> void:
+ # `interrupt(exceptions=...)` should leave only the named running and queued
+ # events alive. Here the front running event and one queued background event
+ # are excepted, while the other front/background events must be cleared.
+ var events = _load_fixture_events("global_interrupt_preserves_exceptions.esc")
+ assert_bool(events.has_event_with_target("front_keep")).is_true()
+ assert_bool(events.has_event_with_target("front_drop")).is_true()
+ assert_bool(events.has_event_with_target("background_drop")).is_true()
+ assert_bool(events.has_event_with_target("background_keep")).is_true()
+ escoria.globals_manager.set_global("interrupt_except_front_result", "start")
+ escoria.globals_manager.set_global("interrupt_except_front_interrupted", false)
+ escoria.globals_manager.set_global("interrupt_except_background_result", "start")
+ escoria.globals_manager.set_global("interrupt_except_background_interrupted", false)
+ escoria.globals_manager.set_global("interrupt_except_background_keep_result", "start")
+
+ var signal_results := {
+ "front_finishes": [],
+ "background_finishes": [],
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ signal_results["front_finishes"].append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ signal_results["background_finishes"].append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_keep"))
+ escoria.event_manager.queue_event(events.get_event_with_target("front_drop"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_drop"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_keep"))
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt(PackedStringArray(["front_keep", "background_keep"]))
+
+ while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].size() < 2:
+ await escoria.get_tree().process_frame
+
+ assert_int(signal_results["front_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_keep")
+ assert_int(signal_results["background_finishes"].size()).is_equal(2)
+ assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_drop")
+ assert_int(int(signal_results["background_finishes"][1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["background_finishes"][1][1])).is_equal("background_keep")
+ assert_str(String(escoria.globals_manager.get_global("interrupt_except_front_result"))).is_equal("front-cmd-after")
+ assert_bool(escoria.globals_manager.get_global("interrupt_except_front_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("interrupt_except_background_result"))).is_equal("start")
+ assert_bool(escoria.globals_manager.get_global("interrupt_except_background_interrupted") == true).is_true()
+ assert_str(String(escoria.globals_manager.get_global("interrupt_except_background_keep_result"))).is_equal("bg-keep-after")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_scheduled_front_event_runs_after_current_front_event_while_background_is_busy() -> void:
+ # Scheduled events always feed the `_front` queue. They should still dispatch
+ # while a background channel is busy, but must wait for the current front
+ # event to finish before the scheduled front event runs.
+ var events = _load_fixture_events("scheduled_front_event_with_busy_background.esc")
+ assert_bool(events.has_event_with_target("front_running")).is_true()
+ assert_bool(events.has_event_with_target("scheduled_front")).is_true()
+ assert_bool(events.has_event_with_target("background_running")).is_true()
+ escoria.globals_manager.set_global("scheduled_front_result", "start")
+ escoria.globals_manager.set_global("scheduled_front_interrupted", false)
+ escoria.globals_manager.set_global("scheduled_background_result", "start")
+ escoria.globals_manager.set_global("scheduled_background_interrupted", false)
+
+ var signal_results := {
+ "front_finishes": [],
+ "background_finishes": [],
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ signal_results["front_finishes"].append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ signal_results["background_finishes"].append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_running"))
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running"))
+ escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.05, "")
+
+ while signal_results["front_finishes"].size() < 2 or signal_results["background_finishes"].is_empty():
+ await escoria.get_tree().process_frame
+
+ assert_int(signal_results["front_finishes"].size()).is_equal(2)
+ assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_running")
+ assert_int(int(signal_results["front_finishes"][1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["front_finishes"][1][1])).is_equal("scheduled_front")
+ assert_int(signal_results["background_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running")
+ assert_str(String(escoria.globals_manager.get_global("scheduled_front_result"))).is_equal("front-running-after-scheduled")
+ assert_bool(escoria.globals_manager.get_global("scheduled_front_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("scheduled_background_result"))).is_equal("background-running-after")
+ assert_bool(escoria.globals_manager.get_global("scheduled_background_interrupted") == true).is_false()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_global_interrupt_preserves_scheduled_events_before_dispatch() -> void:
+ # Global interrupt should stop the currently running event, but it should
+ # not clear scheduled events that have not yet been promoted into the front
+ # queue. The scheduled front event should therefore still dispatch and
+ # complete normally after the interrupted event finishes.
+ var events = _load_fixture_events("scheduled_event_interrupted_before_dispatch.esc")
+ assert_bool(events.has_event_with_target("front_running")).is_true()
+ assert_bool(events.has_event_with_target("scheduled_front")).is_true()
+ escoria.globals_manager.set_global("scheduled_interrupt_result", "start")
+ escoria.globals_manager.set_global("scheduled_interrupt_was_interrupted", false)
+
+ var front_finishes: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ front_finishes.append([return_code, event_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+
+ escoria.event_manager.queue_event(events.get_event_with_target("front_running"))
+ escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.2, "")
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt()
+
+ while front_finishes.is_empty():
+ await escoria.get_tree().process_frame
+
+ await escoria.get_tree().create_timer(0.25).timeout
+
+ assert_int(front_finishes.size()).is_equal(2)
+ assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(front_finishes[0][1])).is_equal("front_running")
+ assert_int(int(front_finishes[1][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(front_finishes[1][1])).is_equal("scheduled_front")
+ assert_str(String(escoria.globals_manager.get_global("scheduled_interrupt_result"))).is_equal("start-scheduled")
+ assert_bool(escoria.globals_manager.get_global("scheduled_interrupt_was_interrupted") == true).is_true()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+
+
+func test_clear_event_queue_preserves_scheduled_events() -> void:
+ # Clearing the event queue should only remove events that are already queued
+ # for execution. Scheduled events live in a separate list until `_process()`
+ # promotes them into the front queue, so they should still dispatch after
+ # `clear_event_queue()` is called.
+ var events = _load_fixture_events("scheduled_event_interrupted_before_dispatch.esc")
+ assert_bool(events.has_event_with_target("scheduled_front")).is_true()
+ escoria.globals_manager.set_global("scheduled_interrupt_result", "start")
+
+ var front_finishes: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ front_finishes.append([return_code, event_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+
+ escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.05, "")
+ escoria.event_manager.clear_event_queue()
+
+ while front_finishes.is_empty():
+ await escoria.get_tree().process_frame
+
+ assert_int(front_finishes.size()).is_equal(1)
+ assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(front_finishes[0][1])).is_equal("scheduled_front")
+ assert_str(String(escoria.globals_manager.get_global("scheduled_interrupt_result"))).is_equal("start-scheduled")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+
+
+func test_interrupt_channel_does_not_affect_still_scheduled_front_events() -> void:
+ # A scheduled front event should be unaffected by `interrupt_channel()` for a
+ # background channel while it is still waiting in `scheduled_events`. Once it
+ # is later promoted into `_front`, it should run like any other front-queue
+ # event.
+ var events = _load_fixture_events("scheduled_front_event_with_busy_background.esc")
+ assert_bool(events.has_event_with_target("scheduled_front")).is_true()
+ assert_bool(events.has_event_with_target("background_running")).is_true()
+ escoria.globals_manager.set_global("scheduled_front_result", "start")
+ escoria.globals_manager.set_global("scheduled_front_interrupted", false)
+ escoria.globals_manager.set_global("scheduled_background_result", "start")
+ escoria.globals_manager.set_global("scheduled_background_interrupted", false)
+
+ var signal_results := {
+ "front_finishes": [],
+ "background_finishes": [],
+ }
+ var on_front_finished := func(return_code, event_name) -> void:
+ signal_results["front_finishes"].append([return_code, event_name])
+ var on_background_finished := func(return_code, event_name, finished_channel_name) -> void:
+ if finished_channel_name == "bg_test":
+ signal_results["background_finishes"].append([return_code, event_name, finished_channel_name])
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+ escoria.event_manager.background_event_finished.connect(on_background_finished)
+
+ escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running"))
+ escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.2, "")
+
+ await escoria.get_tree().create_timer(0.05).timeout
+ escoria.event_manager.interrupt_channel("bg_test")
+
+ while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].is_empty():
+ await escoria.get_tree().process_frame
+
+ assert_int(signal_results["background_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED)
+ assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running")
+ assert_int(signal_results["front_finishes"].size()).is_equal(1)
+ assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK)
+ assert_str(String(signal_results["front_finishes"][0][1])).is_equal("scheduled_front")
+ assert_str(String(escoria.globals_manager.get_global("scheduled_front_result"))).is_equal("start-scheduled")
+ assert_bool(escoria.globals_manager.get_global("scheduled_front_interrupted") == true).is_false()
+ assert_str(String(escoria.globals_manager.get_global("scheduled_background_result"))).is_equal("start")
+ assert_bool(escoria.globals_manager.get_global("scheduled_background_interrupted") == true).is_true()
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ escoria.event_manager.background_event_finished.disconnect(on_background_finished)
+
+
+func test_immediate_command_preserves_statement_ordering() -> void:
+ # A synchronous command should complete before the next statement runs. The
+ # trailing assignment observes the command's mutation and appends to it.
+ var outcome := await _interpret_fixture("immediate_command_ordering.esc")
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("cmd-after")
+
+
+func test_delayed_command_preserves_statement_ordering() -> void:
+ # An asynchronous command must also block statement sequencing. If the
+ # interpreter failed to await it, the trailing assignment would append to the
+ # pre-command value instead of the command-written one.
+ var outcome := await _interpret_fixture("delayed_command_ordering.esc")
+ var globals: Dictionary = outcome.globals
+ assert_bool(globals.has("result")).is_true()
+ assert_str(String(globals["result"])).is_equal("cmd-after")
+
+
+func test_set_animations_updates_persisted_player_resource_on_repeat_calls() -> void:
+ # Room changes restore player animations from the reserved
+ # `ANIMATION_RESOURCES` global. Repeated `set_animations()` calls must update
+ # the stored path each time, otherwise the next room load re-applies a stale
+ # costume even if the live player was changed back already.
+ var previous_animations = escoria.globals_manager.get_global(
+ escoria.room_manager.GLOBAL_ANIMATION_RESOURCES
+ )
+ var room := ESCRoom.new()
+ room.global_id = "test_room_issue_1226"
+ escoria.object_manager.set_current_room(room)
+
+ var player := MARK_SCENE.instantiate() as ESCPlayer
+ player.global_id = "player"
+ escoria.object_manager.register_object(
+ ESCObject.new(player.global_id, player),
+ room,
+ true,
+ false
+ )
+ escoria.globals_manager.set_global(
+ escoria.room_manager.GLOBAL_ANIMATION_RESOURCES,
+ {},
+ true
+ )
+
+ var command := SetAnimationsCommand.new()
+ var jester_animations := "res://game/characters/mark/mark_animations_jester.tres"
+ var default_animations := "res://game/characters/mark/mark_animations.tres"
+
+ assert_int(command.run([player.global_id, jester_animations])).is_equal(ESCExecution.RC_OK)
+ assert_int(command.run([player.global_id, default_animations])).is_equal(ESCExecution.RC_OK)
+
+ var animations: Dictionary = escoria.globals_manager.get_global(
+ escoria.room_manager.GLOBAL_ANIMATION_RESOURCES
+ )
+ assert_bool(animations.has(player.global_id)).is_true()
+ assert_str(String(animations[player.global_id])).is_equal(default_animations)
+
+ var room_key := ESCRoomObjectsKey.new()
+ room_key.room_global_id = room.global_id
+ room_key.room_instance_id = room.get_instance_id()
+ escoria.object_manager.unregister_object_by_global_id(player.global_id, room_key)
+ escoria.globals_manager.set_global(
+ escoria.room_manager.GLOBAL_ANIMATION_RESOURCES,
+ previous_animations if previous_animations != null else {},
+ true
+ )
+ player.free()
+ room.free()
+
+
+func test_set_speed_rejects_negative_values_but_accepts_zero() -> void:
+ var room := ESCRoom.new()
+ room.global_id = "test_room_set_speed_contract"
+ escoria.object_manager.set_current_room(room)
+
+ var player := MARK_SCENE.instantiate() as ESCPlayer
+ player.global_id = "player"
+ escoria.object_manager.register_object(
+ ESCObject.new(player.global_id, player),
+ room,
+ true,
+ false
+ )
+
+ var command := SetSpeedCommand.new()
+ var initial_speed := player.speed
+
+ assert_bool(command.validate([player.global_id, -1])).is_false()
+ assert_int(player.speed).is_equal(initial_speed)
+
+ assert_bool(command.validate([player.global_id, 0])).is_true()
+ assert_int(command.run([player.global_id, 0])).is_equal(ESCExecution.RC_OK)
+ assert_int(player.speed).is_equal(0)
+
+ var room_key := ESCRoomObjectsKey.new()
+ room_key.room_global_id = room.global_id
+ room_key.room_instance_id = room.get_instance_id()
+ escoria.object_manager.unregister_object_by_global_id(player.global_id, room_key)
+ player.free()
+ room.free()
+
+
+func test_trigger_actions_select_targeted_events_for_activating_object() -> void:
+ var script_object := _compile_fixture_script("trigger_targeted_events_use_object_id.esc")
+ var room := ESCRoom.new()
+ room.global_id = "test_trigger_targeted_events_room"
+ escoria.object_manager.set_current_room(room)
+
+ var trigger_node := Node2D.new()
+ var trigger_object := ESCObject.new("trigger_under_test", trigger_node)
+ trigger_object.events = script_object.events
+ escoria.object_manager.register_object(trigger_object, room, true, false)
+
+ escoria.globals_manager.set_global("trigger_in_result", "unset", true)
+ escoria.globals_manager.set_global("trigger_out_result", "unset", true)
+
+ var finished_events: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ if return_code == ESCExecution.RC_OK:
+ finished_events.append(event_name)
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+
+ escoria.action_manager.do(
+ escoria.action_manager.ACTION.TRIGGER_IN,
+ [trigger_object.global_id, "player", "trigger_in"]
+ )
+
+ if not await _wait_for_finished_events(finished_events, 1, "targeted trigger_in"):
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+ return
+
+ assert_str(String(escoria.globals_manager.get_global("trigger_in_result"))).is_equal(
+ "player-in"
+ )
+ assert_str(String(finished_events[0])).is_equal("trigger_in")
+
+ escoria.action_manager.do(
+ escoria.action_manager.ACTION.TRIGGER_OUT,
+ [trigger_object.global_id, "player", "trigger_out"]
+ )
+
+ if not await _wait_for_finished_events(finished_events, 2, "targeted trigger_out"):
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+ return
+
+ assert_str(String(escoria.globals_manager.get_global("trigger_out_result"))).is_equal(
+ "player-out"
+ )
+ assert_str(String(finished_events[1])).is_equal("trigger_out")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+
+
+func test_trigger_actions_fall_back_to_targetless_events() -> void:
+ var script_object := _compile_fixture_script("trigger_targeted_events_use_object_id.esc")
+ var room := ESCRoom.new()
+ room.global_id = "test_trigger_targetless_events_room"
+ escoria.object_manager.set_current_room(room)
+
+ var trigger_node := Node2D.new()
+ var trigger_object := ESCObject.new("trigger_under_test", trigger_node)
+ trigger_object.events = script_object.events
+ escoria.object_manager.register_object(trigger_object, room, true, false)
+
+ escoria.globals_manager.set_global("trigger_in_result", "unset", true)
+ escoria.globals_manager.set_global("trigger_out_result", "unset", true)
+
+ var finished_events: Array = []
+ var on_front_finished := func(return_code, event_name) -> void:
+ if return_code == ESCExecution.RC_OK:
+ finished_events.append(event_name)
+
+ escoria.event_manager.event_finished.connect(on_front_finished)
+
+ escoria.action_manager.do(
+ escoria.action_manager.ACTION.TRIGGER_IN,
+ [trigger_object.global_id, "unexpected_actor", "trigger_in"]
+ )
+
+ if not await _wait_for_finished_events(finished_events, 1, "targetless trigger_in fallback"):
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+ return
+
+ assert_str(String(escoria.globals_manager.get_global("trigger_in_result"))).is_equal(
+ "fallback-in"
+ )
+ assert_str(String(finished_events[0])).is_equal("trigger_in")
+
+ escoria.action_manager.do(
+ escoria.action_manager.ACTION.TRIGGER_OUT,
+ [trigger_object.global_id, "unexpected_actor", "trigger_out"]
+ )
+
+ if not await _wait_for_finished_events(finished_events, 2, "targetless trigger_out fallback"):
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+ return
+
+ assert_str(String(escoria.globals_manager.get_global("trigger_out_result"))).is_equal(
+ "fallback-out"
+ )
+ assert_str(String(finished_events[1])).is_equal("trigger_out")
+
+ escoria.event_manager.event_finished.disconnect(on_front_finished)
+
+ _cleanup_registered_object(trigger_object, trigger_node, room)
+
+
+func _cleanup_registered_object(trigger_object: ESCObject, trigger_node: Node, room: ESCRoom) -> void:
+ var room_key := ESCRoomObjectsKey.new()
+ room_key.room_global_id = room.global_id
+ room_key.room_instance_id = room.get_instance_id()
+ escoria.object_manager.unregister_object_by_global_id(trigger_object.global_id, room_key)
+ trigger_node.free()
+ room.free()
+
+
+func _wait_for_finished_events(
+ finished_events: Array,
+ expected_size: int,
+ context: String
+) -> bool:
+ for _frame in range(120):
+ if finished_events.size() >= expected_size:
+ return true
+
+ await escoria.get_tree().process_frame
+
+ fail(
+ "Timed out waiting for %s; expected %d finished event(s), got %d." % [
+ context,
+ expected_size,
+ finished_events.size()
+ ]
+ )
+ return false
+
+
+func _interpret_fixture(name: String, dialog_choices: Array = []) -> Dictionary:
+ # Shared interpreter harness for fixture-based tests. This intentionally
+ # runs the full scan -> parse -> resolve -> interpret pipeline so the tests
+ # cover the real runtime path rather than isolated visitor calls.
+ var source := _load_fixture(name)
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path(name))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ resolver.resolve(statements)
+ var dialog_player = null
+
+ if not dialog_choices.is_empty():
+ # Install a temporary deterministic dialog player only for tests that
+ # need scripted choices, then clean it up afterward to avoid orphan nodes.
+ dialog_player = DialogPlayerDouble.new(dialog_choices)
+ escoria.set("dialog_player", dialog_player)
+
+ await interpreter.interpret(statements)
+
+ var globals := interpreter.get_global_values().duplicate(true)
+ var dialog_choices_consumed := 0
+ var remaining_dialog_choices := 0
+
+ if dialog_player:
+ dialog_choices_consumed = dialog_player.get_choices_consumed()
+ remaining_dialog_choices = dialog_player.get_remaining_choices()
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+ return {
+ "globals": globals,
+ "dialog_choices_consumed": dialog_choices_consumed,
+ "remaining_dialog_choices": remaining_dialog_choices,
+ }
+
+
+func test_dialog_start_args_are_passed_to_runtime_dialog() -> void:
+ var script_object := _compile_fixture_script("dialog_start_args_reach_runtime.esc")
+ var dialog_player := DialogPlayerDouble.new([1])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(1)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_str(started_dialogs[0]["avatar"]).is_equal("res://game/dialog_avatars/player.tres")
+ assert_int(started_dialogs[0]["timeout"]).is_equal(5)
+ assert_int(started_dialogs[0]["timeout_option"]).is_equal(2)
+ assert_int(started_dialogs[0]["authored_options_count"]).is_equal(2)
+ assert_int(started_dialogs[0]["options_size"]).is_equal(2)
+ assert_str(started_dialogs[0]["visible_timeout_option"]).is_equal("Second option")
+ assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([1, 2])
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_timeout_option_uses_authored_index_after_filtering() -> void:
+ var script_object := _compile_fixture_script("dialog_timeout_visible_authored_default_after_filtering.esc")
+ var dialog_player := DialogPlayerDouble.new([1])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(1)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_int(started_dialogs[0]["timeout"]).is_equal(5)
+ assert_int(started_dialogs[0]["timeout_option"]).is_equal(3)
+ assert_int(started_dialogs[0]["authored_options_count"]).is_equal(3)
+ assert_int(started_dialogs[0]["options_size"]).is_equal(2)
+ assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([2, 3])
+ assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Second option", "Third option"])
+ assert_str(started_dialogs[0]["visible_timeout_option"]).is_equal("Third option")
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_timeout_option_hidden_after_filtering_is_not_available() -> void:
+ var script_object := _compile_fixture_script("dialog_timeout_hidden_authored_default_after_filtering.esc")
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(1)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_int(started_dialogs[0]["timeout"]).is_equal(5)
+ assert_int(started_dialogs[0]["timeout_option"]).is_equal(1)
+ assert_int(started_dialogs[0]["authored_options_count"]).is_equal(3)
+ assert_int(started_dialogs[0]["options_size"]).is_equal(2)
+ assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([2, 3])
+ assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Second option", "Third option"])
+ assert_object(started_dialogs[0]["visible_timeout_option"]).is_null()
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_simple_chooser_does_not_start_timer_when_timeout_default_is_hidden() -> void:
+ var dialog := ESCDialog.new()
+ dialog.timeout = 5
+ dialog.timeout_option = 1
+ dialog.authored_options_count = 2
+ dialog.options = [
+ _make_dialog_option("Visible option", 2),
+ ]
+
+ var chooser = SIMPLE_CHOOSER_SCENE.instantiate()
+ add_child(chooser)
+ chooser.set_dialog(dialog)
+ chooser.show_chooser()
+
+ assert_bool(chooser.get_node("Timer").is_stopped()).is_true()
+ assert_float(chooser.get_node("TimerProgress").value).is_equal(0.0)
+
+ chooser.queue_free()
+
+
+func test_simple_chooser_starts_timer_when_timeout_default_is_visible() -> void:
+ var dialog := ESCDialog.new()
+ dialog.timeout = 5
+ dialog.timeout_option = 2
+ dialog.authored_options_count = 2
+ dialog.options = [
+ _make_dialog_option("First option", 1),
+ _make_dialog_option("Second option", 2),
+ ]
+
+ var chooser = SIMPLE_CHOOSER_SCENE.instantiate()
+ add_child(chooser)
+ chooser.set_dialog(dialog)
+ chooser.show_chooser()
+
+ assert_bool(chooser.get_node("Timer").is_stopped()).is_false()
+
+ chooser.queue_free()
+
+
+func test_dialog_start_invalid_avatar_type_returns_error() -> void:
+ var script_object := _compile_fixture_script("dialog_start_invalid_avatar_type.esc")
+ var dialog_player := DialogPlayerDouble.new([])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(0)
+ assert_int(dialog_player.get_dialogs_started().size()).is_equal(0)
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_start_invalid_timeout_type_returns_error() -> void:
+ var script_object := _compile_fixture_script("dialog_start_invalid_timeout_type.esc")
+ var dialog_player := DialogPlayerDouble.new([])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(0)
+ assert_int(dialog_player.get_dialogs_started().size()).is_equal(0)
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_option_translation_key_is_passed_to_runtime_dialog() -> void:
+ var script_object := _compile_fixture_script("dialog_option_translation_key.esc")
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(1)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["UNCLE_SVEN", ""])
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_start_invalid_timeout_option_type_returns_error() -> void:
+ var script_object := _compile_fixture_script("dialog_start_invalid_timeout_option_type.esc")
+ var dialog_player := DialogPlayerDouble.new([])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(0)
+ assert_int(dialog_player.get_dialogs_started().size()).is_equal(0)
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_start_invalid_timeout_option_negative_returns_error() -> void:
+ var script_object := _compile_fixture_script("dialog_start_invalid_timeout_option_negative.esc")
+ var dialog_player := DialogPlayerDouble.new([])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+ assert_int(dialog_player.get_choices_consumed()).is_equal(0)
+ assert_int(dialog_player.get_dialogs_started().size()).is_equal(0)
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_option_translation_key_beats_legacy_prefix_fallback() -> void:
+ var script_object := _compile_fixture_script("dialog_option_translation_key_beats_legacy_prefix.esc")
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["OUTER_KEY"])
+ assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["INNER_KEY:Fallback text"])
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_option_translation_key_with_condition_survives_runtime_filtering() -> void:
+ var script_object := _compile_fixture_script("dialog_option_translation_key_with_condition.esc")
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_int(started_dialogs[0]["options_size"]).is_equal(1)
+ assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["CONDITIONAL_KEY"])
+ assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Conditional option"])
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func test_dialog_option_translation_key_with_expression_preserves_key_and_value() -> void:
+ var script_object := _compile_fixture_script("dialog_option_translation_key_with_expression.esc")
+ var dialog_player := DialogPlayerDouble.new([0])
+ escoria.set("dialog_player", dialog_player)
+ var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {})
+ var resolver := ESCResolver.new(interpreter)
+ var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk")
+
+ assert_object(event).is_not_null()
+
+ resolver.resolve(event)
+
+ await interpreter.interpret(event)
+
+ var started_dialogs := dialog_player.get_dialogs_started()
+ assert_int(started_dialogs.size()).is_equal(1)
+ assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["DYNAMIC_KEY"])
+ assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Dynamic option"])
+
+ _cleanup_dialog_test(interpreter, dialog_player)
+
+
+func _cleanup_dialog_test(interpreter: ESCInterpreter, dialog_player: DialogPlayerDouble) -> void:
+ interpreter.cleanup()
+
+ if dialog_player:
+ escoria.set("dialog_player", null)
+ dialog_player.queue_free()
+
+
+func test_compiler_rejects_exact_duplicate_event_targets() -> void:
+ var source := _load_fixture("duplicate_exact_target_event_overwrites.esc")
+ var compiler := ESCCompiler.new()
+ var script_object := compiler.compile(source)
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(script_object.events.events.size()).is_equal(1)
+ assert_object(script_object.get_event_with_target("use", "wrench")).is_not_null()
+
+
+func _load_fixture(name: String) -> String:
+ return FileAccess.get_file_as_string(_fixture_path(name))
+
+
+func _load_fixture_events(name: String):
+ var script_object := _compile_fixture_script(name)
+ return script_object.events
+
+
+func _compile_fixture_script(name: String) -> ESCScript:
+ var source := _load_fixture(name)
+ var compiler := ESCCompiler.new()
+ var script_object := compiler.compile(source, _fixture_path(name))
+ assert_bool(compiler.had_error).is_false()
+ return script_object
+
+
+func _fixture_path(name: String) -> String:
+ return "res://addons/escoria-core/testing/ashes/interpreter/fixtures/%s" % name
+
+
+func _make_dialog_option(text: String, source_option_index: int) -> ESCDialogOption:
+ var option := ESCDialogOption.new()
+ option.option = text
+ option.source_option_index = source_option_index
+ option.set_is_valid(true)
+ return option
diff --git a/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid
new file mode 100644
index 0000000..f4c0246
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid
@@ -0,0 +1 @@
+uid://cpogk3hmwoaom
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc
new file mode 100644
index 0000000..6ab1752
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc
@@ -0,0 +1,3 @@
+:repeat_event
+ test_delayed_set_global("background_duplicate_last", "tick", 0.05)
+ inc_global("background_duplicate_count")
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid
new file mode 100644
index 0000000..eeed814
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid
@@ -0,0 +1 @@
+uid://c5o8v1l4n3xmd
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc
new file mode 100644
index 0000000..e4c2d34
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc
@@ -0,0 +1,8 @@
+:test
+ global result = 0
+ var x = 1
+ while true:
+ if true:
+ var x = 2
+ break
+ result = x
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid
new file mode 100644
index 0000000..1060cfd
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid
@@ -0,0 +1 @@
+uid://ccf4pbps7lq2k
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc
new file mode 100644
index 0000000..cb170eb
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc
@@ -0,0 +1,14 @@
+:test
+ global result = "start"
+ ?!
+ - "Outer"
+ ?!
+ - "Middle"
+ ?!
+ - "Break out"
+ break 2
+ result = "after_inner"
+ result = "after_middle"
+ - "Exit outer"
+ break
+ result = result + "-done"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid
new file mode 100644
index 0000000..31dc1f6
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid
@@ -0,0 +1 @@
+uid://dorj4y3h76w2j
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc
new file mode 100644
index 0000000..4e9f649
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc
@@ -0,0 +1,7 @@
+:first
+ global bg_queue_result = "first"
+ test_interruptible_set_global("bg_queue_result", "running", "bg_queue_interrupted", 0.2)
+ bg_queue_result = bg_queue_result + "-after"
+
+:second
+ bg_queue_result = "second"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid
new file mode 100644
index 0000000..45a95b6
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid
@@ -0,0 +1 @@
+uid://dp5nx4h5bqgu1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc
new file mode 100644
index 0000000..e3130d4
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc
@@ -0,0 +1,9 @@
+:front_event
+ global channel_front_result = "start"
+ test_interruptible_set_global("channel_front_result", "front-cmd", "channel_front_was_interrupted", 0.2)
+ channel_front_result = channel_front_result + "-after"
+
+:background_event
+ global channel_background_result = "start"
+ test_interruptible_set_global("channel_background_result", "bg-cmd", "channel_background_was_interrupted", 0.2)
+ channel_background_result = channel_background_result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid
new file mode 100644
index 0000000..5e3ad25
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid
@@ -0,0 +1 @@
+uid://cx76t0u0r5f4f
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc
new file mode 100644
index 0000000..31098f8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc
@@ -0,0 +1,12 @@
+:front_event
+ global isolated_front_result = "start"
+ test_interruptible_set_global("isolated_front_result", "front-cmd", "isolated_front_interrupted", 0.2)
+ isolated_front_result = isolated_front_result + "-after"
+
+:background_first
+ global isolated_background_result = "start"
+ test_interruptible_set_global("isolated_background_result", "first-cmd", "isolated_background_interrupted", 0.2)
+ isolated_background_result = isolated_background_result + "-after-first"
+
+:background_second
+ isolated_background_result = isolated_background_result + "-second"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid
new file mode 100644
index 0000000..7961883
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid
@@ -0,0 +1 @@
+uid://dzq6qk1ufhcv0
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc
new file mode 100644
index 0000000..3df3b97
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc
@@ -0,0 +1,7 @@
+:front_waits_for_new_global
+ global concurrent_visibility_result = "start"
+ test_delayed_set_global("concurrent_visibility_progress", "waiting", 0.2)
+ concurrent_visibility_result = shared_runtime_global
+
+:background_declares_new_global
+ global shared_runtime_global = "from-background"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid
new file mode 100644
index 0000000..f19670e
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid
@@ -0,0 +1 @@
+uid://a226b7bb41b142b5812d0d3dbd389a31
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc
new file mode 100644
index 0000000..2ae480f
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc
@@ -0,0 +1,7 @@
+:test
+ global result = "unset"
+ ?!
+ - "Run delayed"
+ test_delayed_set_global("result", "cmd", 0.01)
+ break
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid
new file mode 100644
index 0000000..d89858e
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid
@@ -0,0 +1 @@
+uid://e0743a93c87c4db4af251658723ed469
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc
new file mode 100644
index 0000000..e55b9a6
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc
@@ -0,0 +1,7 @@
+:test
+ global result = "start"
+ global count = 0
+ while count < 1:
+ test_delayed_set_global("result", "cmd", 0.01)
+ count = count + 1
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid
new file mode 100644
index 0000000..e832298
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid
@@ -0,0 +1 @@
+uid://c08224d490434f5dbe8529aa021c6041
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc
new file mode 100644
index 0000000..5aa9aed
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc
@@ -0,0 +1,5 @@
+:test
+ global result = "start"
+ global was_interrupted = false
+ test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2)
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc
new file mode 100644
index 0000000..e9187b1
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc
@@ -0,0 +1,7 @@
+:test
+ global result = "start"
+ global was_interrupted = false
+ ?!
+ - "Wait"
+ test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2)
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc
new file mode 100644
index 0000000..fc19824
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc
@@ -0,0 +1,8 @@
+:test
+ global result = "start"
+ global was_interrupted = false
+ global count = 0
+ while count < 3:
+ test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2)
+ count = count + 1
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc
new file mode 100644
index 0000000..52ef2a4
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc
@@ -0,0 +1,4 @@
+:test
+ global result = "unset"
+ test_delayed_set_global("result", "cmd", 0.01)
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid
new file mode 100644
index 0000000..a014454
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid
@@ -0,0 +1 @@
+uid://bl7s16m8hlyj1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc
new file mode 100644
index 0000000..eaf762e
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc
@@ -0,0 +1,14 @@
+:talk
+ global result = "start"
+ global option1_is_valid = true
+ global option2_is_valid = true
+
+ ?!
+ - "option1" [option1_is_valid]
+ result = result + "-say1"
+ option1_is_valid = false
+ - "option2" [option2_is_valid]
+ result = result + "-say2"
+ option2_is_valid = false
+
+ result = result + "-finished"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid
new file mode 100644
index 0000000..fac2cb3
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid
@@ -0,0 +1 @@
+uid://cn1f4l17yg4x8
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc
new file mode 100644
index 0000000..69f88db
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc
@@ -0,0 +1,6 @@
+:talk
+ ?!
+ - UNCLE_SVEN:"Uncle Sven sends regards."
+ break
+ - "Fallback"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid
new file mode 100644
index 0000000..d371627
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid
@@ -0,0 +1 @@
+uid://dcjcq0w2qj1h2
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc
new file mode 100644
index 0000000..1be9d33
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc
@@ -0,0 +1,4 @@
+:talk
+ ?!
+ - OUTER_KEY:"INNER_KEY:Fallback text"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid
new file mode 100644
index 0000000..aff1543
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid
@@ -0,0 +1 @@
+uid://cb71rbl5a7hlj
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc
new file mode 100644
index 0000000..718a5fc
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc
@@ -0,0 +1,7 @@
+:talk
+ global show_first = true
+ ?!
+ - CONDITIONAL_KEY:"Conditional option" [show_first]
+ break
+ - HIDDEN_KEY:"Hidden option" [false]
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid
new file mode 100644
index 0000000..4f944b1
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid
@@ -0,0 +1 @@
+uid://bnep8t7ivgu7f
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc
new file mode 100644
index 0000000..587bd39
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc
@@ -0,0 +1,5 @@
+:talk
+ var option_text = "Dynamic option"
+ ?!
+ - DYNAMIC_KEY:option_text
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid
new file mode 100644
index 0000000..56eee44
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid
@@ -0,0 +1 @@
+uid://bf67f4h73tba2
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc
new file mode 100644
index 0000000..924cf52
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc
@@ -0,0 +1,9 @@
+:test
+ global result = "start"
+ var x = "outer"
+ ?!
+ - "Use inner"
+ var x = "inner"
+ result = x
+ break
+ result = result + ":" + x
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc
new file mode 100644
index 0000000..aab29c6
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc
@@ -0,0 +1,6 @@
+:talk
+ ?! "res://game/dialog_avatars/player.tres" 5 2
+ - "First option"
+ pass
+ - "Second option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid
new file mode 100644
index 0000000..8a474a9
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid
@@ -0,0 +1 @@
+uid://b3a4m3l6nr6m7
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc
new file mode 100644
index 0000000..368ef0b
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc
@@ -0,0 +1,4 @@
+:talk
+ ?! 123 5 2
+ - "Only option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid
new file mode 100644
index 0000000..5708f33
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid
@@ -0,0 +1 @@
+uid://chev11x0d0lg0
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc
new file mode 100644
index 0000000..06cab55
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc
@@ -0,0 +1,4 @@
+:talk
+ ?! "res://game/dialog_avatars/player.tres" 5 0 - 1
+ - "Only option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid
new file mode 100644
index 0000000..a7cf6b8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid
@@ -0,0 +1 @@
+uid://db2w5uh1qvqst
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc
new file mode 100644
index 0000000..bc71bc7
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc
@@ -0,0 +1,4 @@
+:talk
+ ?! "res://game/dialog_avatars/player.tres" 5 "two"
+ - "Only option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid
new file mode 100644
index 0000000..deb663d
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid
@@ -0,0 +1 @@
+uid://b5fstn4fo0igo
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc
new file mode 100644
index 0000000..351be56
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc
@@ -0,0 +1,4 @@
+:talk
+ ?! "res://game/dialog_avatars/player.tres" "five" 2
+ - "Only option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid
new file mode 100644
index 0000000..db53e65
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid
@@ -0,0 +1 @@
+uid://cy8r7l4o3dmn0
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc
new file mode 100644
index 0000000..80f8ef7
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc
@@ -0,0 +1,10 @@
+:talk
+ global show_first = false
+
+ ?! "-" 5 1
+ - "First option" [show_first]
+ break
+ - "Second option"
+ break
+ - "Third option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc
new file mode 100644
index 0000000..0067327
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc
@@ -0,0 +1,10 @@
+:talk
+ global show_first = false
+
+ ?! "-" 5 3
+ - "First option" [show_first]
+ break
+ - "Second option"
+ break
+ - "Third option"
+ break
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc
new file mode 100644
index 0000000..770224a
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc
@@ -0,0 +1,11 @@
+:test
+ global result = "start"
+ ?!
+ - "Outer"
+ ?!
+ - "Done"
+ done
+ result = "after_nested"
+ - "Exit outer"
+ break
+ result = result + "-done"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid
new file mode 100644
index 0000000..9b3eaf5
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid
@@ -0,0 +1 @@
+uid://catfpqk5u8vj0
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc
new file mode 100644
index 0000000..a37b050
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc
@@ -0,0 +1,7 @@
+:test
+ global result = 0
+ ?!
+ - "Outer"
+ if true:
+ done
+ result = 1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid
new file mode 100644
index 0000000..8e5c281
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid
@@ -0,0 +1 @@
+uid://bgxmdpje356jd
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc
new file mode 100644
index 0000000..8461cff
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc
@@ -0,0 +1,5 @@
+:use "wrench"
+ test_set_global("result", "first")
+
+:use "wrench"
+ test_set_global("result", "second")
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid
new file mode 100644
index 0000000..600a3bf
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid
@@ -0,0 +1 @@
+uid://cfpnxq6v1bg1t
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc
new file mode 100644
index 0000000..e11c818
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc
@@ -0,0 +1,15 @@
+:front_running
+ global global_interrupt_front_result = "start"
+ test_interruptible_set_global("global_interrupt_front_result", "front-cmd", "global_interrupt_front_was_interrupted", 0.2)
+ global_interrupt_front_result = global_interrupt_front_result + "-after"
+
+:front_queued
+ global_interrupt_front_result = "front-queued"
+
+:background_running
+ global global_interrupt_background_result = "start"
+ test_interruptible_set_global("global_interrupt_background_result", "bg-cmd", "global_interrupt_background_was_interrupted", 0.2)
+ global_interrupt_background_result = global_interrupt_background_result + "-after"
+
+:background_queued
+ global_interrupt_background_result = "bg-queued"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid
new file mode 100644
index 0000000..5547f25
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid
@@ -0,0 +1 @@
+uid://cje3uy4r8lh1x
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc
new file mode 100644
index 0000000..a9ffa26
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc
@@ -0,0 +1,17 @@
+:front_keep
+ global interrupt_except_front_result = "start"
+ test_interruptible_set_global("interrupt_except_front_result", "front-cmd", "interrupt_except_front_interrupted", 0.2)
+ interrupt_except_front_result = interrupt_except_front_result + "-after"
+
+:front_drop
+ interrupt_except_front_result = "front-drop"
+
+:background_drop
+ global interrupt_except_background_result = "start"
+ test_interruptible_set_global("interrupt_except_background_result", "bg-drop", "interrupt_except_background_interrupted", 0.2)
+ interrupt_except_background_result = interrupt_except_background_result + "-after"
+
+:background_keep
+ global interrupt_except_background_keep_result = "start"
+ test_delayed_set_global("interrupt_except_background_keep_result", "bg-keep", 0.01)
+ interrupt_except_background_keep_result = interrupt_except_background_keep_result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid
new file mode 100644
index 0000000..dc38433
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid
@@ -0,0 +1 @@
+uid://ceu2x1lbwfxby
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc
new file mode 100644
index 0000000..7d39c5f
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc
@@ -0,0 +1,4 @@
+:test
+ global result = "unset"
+ test_set_global("result", "cmd")
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid
new file mode 100644
index 0000000..16cf665
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid
@@ -0,0 +1 @@
+uid://dmf5wqf50nfnr
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc
new file mode 100644
index 0000000..fd1ba03
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc
@@ -0,0 +1,11 @@
+:first
+ global interpreter_runtime_result = "first"
+
+:second
+ interpreter_runtime_result = interpreter_runtime_result + "-second"
+
+:background_seed
+ global interpreter_runtime_background_result = "first"
+
+:background_after
+ interpreter_runtime_background_result = interpreter_runtime_background_result + "-background"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid
new file mode 100644
index 0000000..cafce06
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid
@@ -0,0 +1 @@
+uid://clqs3e5r4yem1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc
new file mode 100644
index 0000000..4d92d26
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc
@@ -0,0 +1,4 @@
+:test
+ global result = "start"
+ missing_test_command()
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc
new file mode 100644
index 0000000..2d59fb8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc
@@ -0,0 +1,6 @@
+:test
+ global result = "start"
+ ?!
+ - "Break it"
+ missing_command()
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid
new file mode 100644
index 0000000..77a66d7
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid
@@ -0,0 +1 @@
+uid://d4esrhlw7qhri
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc
new file mode 100644
index 0000000..4ecf824
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc
@@ -0,0 +1,7 @@
+:test
+ global result = "start"
+ global count = 0
+ while count < 1:
+ missing_command()
+ count = count + 1
+ result = result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid
new file mode 100644
index 0000000..ec48741
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid
@@ -0,0 +1 @@
+uid://ci5h4hpy7mjav
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc
new file mode 100644
index 0000000..a81c4c0
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc
@@ -0,0 +1,3 @@
+:test
+ global result = "unset"
+ result = "a" > 1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid
new file mode 100644
index 0000000..67b9e76
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid
@@ -0,0 +1 @@
+uid://bo4clnuxdy778
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc
new file mode 100644
index 0000000..10b036b
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc
@@ -0,0 +1,8 @@
+:look
+ say("player", "It's a leaking pipe.")
+
+:use "r5_wrench"
+ say("player", "The leak's now fixed.")
+
+:use "r5_pen"
+ say("player", "I don't see how I can fix the pipe with a pen.")
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid
new file mode 100644
index 0000000..f77b460
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid
@@ -0,0 +1 @@
+uid://cpdc2ha8v6svh
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc
new file mode 100644
index 0000000..82d638d
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc
@@ -0,0 +1,11 @@
+:test
+ global result = "start"
+ ?!
+ - "Outer"
+ ?!
+ - "Break nested"
+ break 1
+ result = "after_nested"
+ - "Exit outer"
+ break
+ result = result + "-done"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid
new file mode 100644
index 0000000..8d392b8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid
@@ -0,0 +1 @@
+uid://bfwg7dby8q4g3
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc
new file mode 100644
index 0000000..07b5ccd
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc
@@ -0,0 +1,2 @@
+:test
+ print("a", "b")
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid
new file mode 100644
index 0000000..cd16f5f
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid
@@ -0,0 +1 @@
+uid://d0cv0a5fo7hdh
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc
new file mode 100644
index 0000000..3ff88bf
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc
@@ -0,0 +1,2 @@
+:test
+ print()
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid
new file mode 100644
index 0000000..8dd21c3
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid
@@ -0,0 +1 @@
+uid://cc274jba2egui
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc
new file mode 100644
index 0000000..853a985
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc
@@ -0,0 +1,10 @@
+:background_first
+ global block_wait_result = "start"
+ test_delayed_set_global("block_wait_result", "first", 0.05)
+
+:background_target
+ test_delayed_set_global("block_wait_result", "first-target", 0.2)
+
+:front_event
+ global block_wait_front_result = "start"
+ test_delayed_set_global("block_wait_front_result", "front", 0.01)
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid
new file mode 100644
index 0000000..928dcef
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid
@@ -0,0 +1 @@
+uid://4g0g2m5j3ve7u
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc
new file mode 100644
index 0000000..9ea46b0
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc
@@ -0,0 +1,10 @@
+:front_first
+ global front_block_wait_result = "start"
+ test_delayed_set_global("front_block_wait_result", "first", 0.05)
+
+:front_target
+ test_delayed_set_global("front_block_wait_result", "first-target", 0.2)
+
+:background_event
+ global front_block_wait_background_result = "start"
+ test_delayed_set_global("front_block_wait_background_result", "background", 0.01)
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid
new file mode 100644
index 0000000..b725b6c
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid
@@ -0,0 +1 @@
+uid://c5q5hi1sp5u4h
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc
new file mode 100644
index 0000000..475c523
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc
@@ -0,0 +1,7 @@
+:front_running
+ global scheduled_interrupt_result = "start"
+ test_interruptible_set_global("scheduled_interrupt_result", "front-running", "scheduled_interrupt_was_interrupted", 0.2)
+ scheduled_interrupt_result = scheduled_interrupt_result + "-after"
+
+:scheduled_front
+ scheduled_interrupt_result = scheduled_interrupt_result + "-scheduled"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid
new file mode 100644
index 0000000..1ca4eb5
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid
@@ -0,0 +1 @@
+uid://dd1tktxwp4t0s
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc
new file mode 100644
index 0000000..1f88f71
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc
@@ -0,0 +1,12 @@
+:front_running
+ global scheduled_front_result = "start"
+ test_interruptible_set_global("scheduled_front_result", "front-running", "scheduled_front_interrupted", 0.2)
+ scheduled_front_result = scheduled_front_result + "-after"
+
+:scheduled_front
+ scheduled_front_result = scheduled_front_result + "-scheduled"
+
+:background_running
+ global scheduled_background_result = "start"
+ test_interruptible_set_global("scheduled_background_result", "background-running", "scheduled_background_interrupted", 0.2)
+ scheduled_background_result = scheduled_background_result + "-after"
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid
new file mode 100644
index 0000000..235d8c4
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid
@@ -0,0 +1 @@
+uid://cqt1mxg5k7q6m
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc
new file mode 100644
index 0000000..7be3f85
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc
@@ -0,0 +1,6 @@
+:test
+ global result = 0
+ ?!
+ - "Stop"
+ stop
+ result = 1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid
new file mode 100644
index 0000000..d979742
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid
@@ -0,0 +1 @@
+uid://148e3f0660144f8a9e00065bdde07cde
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc
new file mode 100644
index 0000000..52ea799
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc
@@ -0,0 +1,5 @@
+:test
+ global result = 0
+ if true:
+ stop
+ result = 1
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid
new file mode 100644
index 0000000..f005816
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid
@@ -0,0 +1 @@
+uid://ccw3t8r0s6n6r
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc
new file mode 100644
index 0000000..728ca59
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc
@@ -0,0 +1,11 @@
+:trigger_in "player"
+ test_set_global("trigger_in_result", "player-in")
+
+:trigger_in
+ test_set_global("trigger_in_result", "fallback-in")
+
+:trigger_out "player"
+ test_set_global("trigger_out_result", "player-out")
+
+:trigger_out
+ test_set_global("trigger_out_result", "fallback-out")
diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid
new file mode 100644
index 0000000..46d6dcb
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid
@@ -0,0 +1 @@
+uid://ciepdqqwhcgtf
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd
new file mode 100644
index 0000000..c0463b2
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd
@@ -0,0 +1,16 @@
+class_name TestDelayedSetGlobalCommand
+extends ESCBaseCommand
+
+
+func configure() -> ESCCommandArgumentDescriptor:
+ return ESCCommandArgumentDescriptor.new(
+ 3,
+ [[TYPE_STRING], [TYPE_STRING], [TYPE_FLOAT, TYPE_INT]],
+ [null, null, null]
+ )
+
+
+func run(command_params: Array) -> int:
+ await escoria.get_tree().create_timer(float(command_params[2])).timeout
+ escoria.globals_manager.set_global(command_params[0], command_params[1])
+ return ESCExecution.RC_OK
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid
new file mode 100644
index 0000000..f844e2a
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid
@@ -0,0 +1 @@
+uid://b21sl821n61w4
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd
new file mode 100644
index 0000000..2ea34a9
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd
@@ -0,0 +1,32 @@
+class_name TestInterruptibleSetGlobalCommand
+extends ESCBaseCommand
+
+
+var _interrupted := false
+
+
+func configure() -> ESCCommandArgumentDescriptor:
+ return ESCCommandArgumentDescriptor.new(
+ 4,
+ [[TYPE_STRING], [TYPE_STRING], [TYPE_STRING], [TYPE_FLOAT, TYPE_INT]],
+ [null, null, null, null]
+ )
+
+
+func run(command_params: Array) -> int:
+ _interrupted = false
+
+ var deadline_usec := Time.get_ticks_usec() + int(float(command_params[3]) * 1000000.0)
+ while Time.get_ticks_usec() < deadline_usec and not _interrupted:
+ await escoria.get_tree().create_timer(0.01).timeout
+
+ if _interrupted:
+ escoria.globals_manager.set_global(command_params[2], true)
+ return ESCExecution.RC_OK
+
+ escoria.globals_manager.set_global(command_params[0], command_params[1])
+ return ESCExecution.RC_OK
+
+
+func interrupt():
+ _interrupted = true
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd
new file mode 100644
index 0000000..41bd624
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd
@@ -0,0 +1,15 @@
+class_name TestSetGlobalCommand
+extends ESCBaseCommand
+
+
+func configure() -> ESCCommandArgumentDescriptor:
+ return ESCCommandArgumentDescriptor.new(
+ 2,
+ [[TYPE_STRING], [TYPE_STRING]],
+ [null, null]
+ )
+
+
+func run(command_params: Array) -> int:
+ escoria.globals_manager.set_global(command_params[0], command_params[1])
+ return ESCExecution.RC_OK
diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid
new file mode 100644
index 0000000..b273d48
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid
@@ -0,0 +1 @@
+uid://chb08gv4i2nlw
diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd
new file mode 100644
index 0000000..8a2539c
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd
@@ -0,0 +1,586 @@
+# GdUnit generated TestSuite
+class_name ESCParserTest
+extends GdUnitTestSuite
+
+@warning_ignore("unused_parameter")
+@warning_ignore("return_value_discarded")
+
+
+func test_done_statement_does_not_consume_tokens_on_partial_match() -> void:
+ # This fixture is intentionally valid up to the dialog option body:
+ # `done` is in a dialog context, but `done x` is invalid because the parser
+ # only accepts `done` followed immediately by a newline.
+ var source := _load_fixture("done_partial_match.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("done_partial_match.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var done_index := _find_token_index(tokens, ESCTokenType.TokenType.DONE)
+ var print_index := _find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "print")
+ assert_int(done_index).is_greater_equal(0)
+ assert_int(print_index).is_greater_equal(0)
+
+ # Jump directly to the `done` token so this test exercises the statement
+ # parser in isolation. We also force dialog context because `done` is only
+ # legal inside dialogs.
+ parser._current = done_index
+ parser._dialog_level = 1
+
+ var statement = parser._statement()
+
+ # We expect a parse error, but we specifically do not want the parser to
+ # consume the `done` token before it discovers that the following token is
+ # not a newline. If it advances here, later recovery code starts from the
+ # wrong token and can corrupt the remaining parse.
+ assert_object(statement).is_instanceof(ESCParseError)
+ assert_bool(compiler.had_error).is_true()
+ assert_int(parser._current).is_equal(done_index)
+ assert_int(parser._peek().get_type()).is_equal(ESCTokenType.TokenType.DONE)
+ assert_int(tokens[print_index].get_type()).is_equal(ESCTokenType.TokenType.IDENTIFIER)
+ assert_str(tokens[print_index].get_lexeme()).is_equal("print")
+
+ # Tightening the regression: the next statement must still be present in the
+ # token stream after the failed `done` parse attempt. This protects against
+ # accidental over-consumption beyond the failing statement.
+ assert_int(_find_token_index_after(tokens, parser._current, ESCTokenType.TokenType.IDENTIFIER, "print")).is_equal(print_index)
+
+
+func test_parser_synchronizes_after_invalid_done_and_keeps_following_event() -> void:
+ # This fixture places an invalid `done` at the start of an event body and
+ # then follows it with a valid statement and a second event. The purpose is
+ # to verify parser recovery at ASHES statement and event boundaries.
+ var source := _load_fixture("synchronize_after_invalid_done.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("synchronize_after_invalid_done.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ # The parser should report an error for `done x`, but it should still recover
+ # well enough to continue parsing later declarations. In particular, the
+ # following event `:second` must survive recovery.
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_parser_synchronizes_after_invalid_if_and_keeps_following_event() -> void:
+ # This fixture uses a malformed `if` statement inside an event body. The
+ # missing colon forces a real statement parse error after the parser has
+ # already committed to the `if` branch. Recovery must still preserve the
+ # following top-level event declaration.
+ var source := _load_fixture("synchronize_after_invalid_if.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("synchronize_after_invalid_if.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_dialog_option_missing_right_square_returns_parse_error() -> void:
+ # This fixture omits the closing `]` from a dialog option predicate. The
+ # parser should report a parse error here rather than silently building a
+ # malformed dialog option node and continuing as if the condition was valid.
+ # Recovery should still preserve the following top-level event declaration.
+ var source := _load_fixture("dialog_option_missing_right_square.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_option_missing_right_square.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+
+ var found_second := false
+ for statement in statements:
+ if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second":
+ found_second = true
+ break
+
+ assert_bool(found_second).is_true()
+
+
+func test_dialog_option_missing_indented_body_returns_parse_error() -> void:
+ # This fixture omits the required indented body after a dialog option. The
+ # parser should surface the missing block-start error instead of continuing
+ # into `_block()` and constructing a malformed option node.
+ var source := _load_fixture("dialog_option_missing_indented_body.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_option_missing_indented_body.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+
+ var found_second := false
+ for statement in statements:
+ if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second":
+ found_second = true
+ break
+
+ assert_bool(found_second).is_true()
+
+
+func test_dialog_start_arguments_without_parens_parse_and_are_preserved() -> void:
+ # The public docs show dialog-start arguments in space-separated form:
+ # `?! "res://avatar" 5 2`. This regression locks that syntax down so the
+ # parser preserves those arguments instead of requiring the older `?!(...)`
+ # form.
+ var source := _load_fixture("dialog_start_args_without_parens.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_start_args_without_parens.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(2)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+
+ var talk_event := statements[0] as ESCGrammarStmts.Event
+ var talk_block := talk_event.get_body() as ESCGrammarStmts.Block
+ assert_int(talk_block.get_statements().size()).is_equal(1)
+ assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog)
+
+ var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog
+ var args := dialog.get_args()
+ assert_int(args.size()).is_equal(3)
+ assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres")
+ assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_float(args[1].get_value()).is_equal(5.0)
+ assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_float(args[2].get_value()).is_equal(2.0)
+
+
+func test_dialog_start_arguments_with_parens_parse_and_are_preserved() -> void:
+ # Preserve the older parenthesized positional form as well, so both
+ # positional dialog-start syntaxes remain supported.
+ var source := _load_fixture("dialog_start_args_with_parens.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_start_args_with_parens.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(2)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+
+ var talk_event := statements[0] as ESCGrammarStmts.Event
+ var talk_block := talk_event.get_body() as ESCGrammarStmts.Block
+ assert_int(talk_block.get_statements().size()).is_equal(1)
+ assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog)
+
+ var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog
+ var args := dialog.get_args()
+ assert_int(args.size()).is_equal(3)
+ assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres")
+ assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_float(args[1].get_value()).is_equal(5.0)
+ assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal)
+ assert_float(args[2].get_value()).is_equal(2.0)
+
+
+func test_dialog_option_translation_key_is_parsed_explicitly() -> void:
+ var source := _load_fixture("dialog_option_translation_key.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_option_translation_key.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(1)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+
+ var talk_event := statements[0] as ESCGrammarStmts.Event
+ var talk_block := talk_event.get_body() as ESCGrammarStmts.Block
+ var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog
+ var first_option := dialog.get_options()[0] as ESCGrammarStmts.DialogOption
+ var second_option := dialog.get_options()[1] as ESCGrammarStmts.DialogOption
+
+ assert_str(first_option.get_translation_key()).is_equal("UNCLE_SVEN")
+ assert_str(second_option.get_translation_key()).is_equal("")
+
+
+func test_grouping_missing_right_paren_returns_parse_error() -> void:
+ # This fixture omits the closing `)` from a grouped expression inside a
+ # function call. The parser must not silently accept the grouping and build
+ # a malformed AST; it should report the error and still preserve the
+ # following event declaration after recovery.
+ var source := _load_fixture("grouping_missing_right_paren.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("grouping_missing_right_paren.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_var_grouping_missing_right_paren_returns_parse_error() -> void:
+ # This fixture omits the closing `)` from a grouped expression in a variable
+ # initializer. Unlike the call-expression regression above, this path goes
+ # straight through `_primary()`, so the parser must reject the malformed
+ # grouping instead of silently constructing a `Grouping` node.
+ var source := _load_fixture("var_grouping_missing_right_paren.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("var_grouping_missing_right_paren.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_var_declaration_missing_newline_returns_parse_error() -> void:
+ # This fixture continues a variable declaration onto the same line with a
+ # second statement. The parser should not silently accept the declaration
+ # when the required trailing newline is missing, and recovery should still
+ # preserve the following event.
+ var source := _load_fixture("var_missing_newline.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("var_missing_newline.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_global_declaration_missing_newline_returns_parse_error() -> void:
+ # This fixture continues a global declaration onto the same line with a
+ # second statement. The parser should reject the malformed declaration
+ # instead of constructing a partial `Global` node, and recovery should still
+ # preserve the following event.
+ var source := _load_fixture("global_missing_newline.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("global_missing_newline.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(2)
+ assert_object(statements[0]).is_not_null()
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_not_null()
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_while_error_does_not_leak_loop_level_into_later_event() -> void:
+ # This fixture forces `_while_statement()` to fail after incrementing
+ # `_loop_level`. The later `break` in `:second` is top-level within that
+ # event body and must still be rejected. If the loop counter leaks across the
+ # error path, the parser will incorrectly accept it as a valid `Break`.
+ var source := _load_fixture("while_error_then_top_level_break.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("while_error_then_top_level_break.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(3)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+ assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[2].get_event_name()).is_equal("third")
+
+ var second_body: Array = statements[1].get_body().get_statements()
+ assert_int(second_body.size()).is_equal(0)
+
+
+func test_dialog_error_does_not_leak_dialog_level_into_later_event() -> void:
+ # This fixture forces `_dialog_statement()` to fail after incrementing
+ # `_dialog_level`. The later `done` in `:second` is top-level within that
+ # event body and must still be rejected. If the dialog counter leaks across
+ # the error path, the parser will incorrectly accept it as a valid `Done`.
+ var source := _load_fixture("dialog_error_then_top_level_done.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("dialog_error_then_top_level_done.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_true()
+ assert_int(statements.size()).is_greater_equal(3)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+ assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[2].get_event_name()).is_equal("third")
+
+ var second_body: Array = statements[1].get_body().get_statements()
+ assert_int(second_body.size()).is_equal(0)
+
+
+func test_happy_path_full_language_fixture_parses_without_errors() -> void:
+ # This smoke fixture exercises a broad cross-section of ASHES syntax on the
+ # parser happy path: event flags, targets, globals, locals, branching,
+ # looping, built-ins, dialog, nested dialog, conditional dialog options,
+ # `break`, `done`, `in inventory`, `is active`, and `is "state"`.
+ var source := _load_fixture("happy_path_full_language.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("happy_path_full_language.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(2)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("setup")
+ assert_int(statements[0].get_num_statements_in_block()).is_equal(6)
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("look")
+ assert_str(statements[1].get_target_name()).is_equal("worker")
+ assert_int(statements[1].get_num_statements_in_block()).is_equal(3)
+
+
+func test_keyword_prefixed_names_scan_as_identifiers() -> void:
+ # ASHES keywords should only match on exact identifier text. Names that
+ # merely start with keyword text must still scan as ordinary identifiers.
+ var source := _load_fixture("identifier_keyword_prefixes.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("identifier_keyword_prefixes.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "breakfast")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "donee")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "elseify")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "notable")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.BREAK, "break")).is_equal(-1)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.DONE, "done")).is_equal(-1)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.ELSE, "else")).is_equal(-1)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NOT, "not")).is_equal(-1)
+ assert_int(statements.size()).is_equal(1)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+
+
+func test_hyphenated_global_identifier_scans_as_single_identifier() -> void:
+ # `$`-prefixed global-ID shorthand may contain hyphens, but ordinary `-`
+ # characters must still scan as subtraction operators outside that prefix.
+ var source := _load_fixture("hyphenated_global_identifier.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("hyphenated_global_identifier.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "$closet-door")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.MINUS, "-")).is_greater_equal(0)
+ assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NUMBER, "1")).is_greater_equal(0)
+ assert_int(statements.size()).is_equal(1)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+
+
+func test_blank_lines_in_nested_blocks_do_not_break_later_events() -> void:
+ # Blank lines inside nested blocks should not corrupt INDENT/DEDENT handling.
+ # The parser must still recover the later top-level event after the nested
+ # block ends.
+ var source := _load_fixture("blank_lines_in_nested_blocks.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("blank_lines_in_nested_blocks.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(2)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+
+
+func test_blank_lines_before_dedent_do_not_hide_block_termination() -> void:
+ # Blank lines immediately before a block-closing dedent should not be eaten
+ # in a way that hides the dedent from `_block()`. The outer statement after
+ # the nested block, plus the later top-level event, must still parse.
+ var source := _load_fixture("blank_lines_before_dedent.esc")
+ var compiler := ESCCompiler.new()
+ var scanner := ESCScanner.new()
+ scanner.set_source(source)
+ scanner.set_filename(_fixture_path("blank_lines_before_dedent.esc"))
+
+ var tokens: Array = scanner.scan_tokens()
+ var parser := ESCParser.new()
+ parser.init(compiler, tokens, "")
+ var statements := parser.parse()
+
+ assert_bool(compiler.had_error).is_false()
+ assert_int(statements.size()).is_equal(2)
+ assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[0].get_event_name()).is_equal("test")
+ assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event)
+ assert_str(statements[1].get_event_name()).is_equal("second")
+ assert_int(statements[0].get_num_statements_in_block()).is_equal(2)
+
+
+func _load_fixture(name: String) -> String:
+ return FileAccess.get_file_as_string(_fixture_path(name))
+
+
+func _fixture_path(name: String) -> String:
+ return "res://addons/escoria-core/testing/ashes/parser/fixtures/%s" % name
+
+
+func _find_token_index(tokens: Array, token_type: int, lexeme: String = "") -> int:
+ for index in range(tokens.size()):
+ var token = tokens[index]
+ if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme):
+ return index
+
+ return -1
+
+
+func _find_token_index_after(tokens: Array, current_index: int, token_type: int, lexeme: String = "") -> int:
+ for index in range(current_index + 1, tokens.size()):
+ var token = tokens[index]
+ if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme):
+ return index
+
+ return -1
diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid
new file mode 100644
index 0000000..e97a2bb
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid
@@ -0,0 +1 @@
+uid://di2o7dahssqsc
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc
new file mode 100644
index 0000000..0ba7826
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc
@@ -0,0 +1,9 @@
+:test
+ if true:
+ print("inside")
+
+
+ print("after")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid
new file mode 100644
index 0000000..4287f2f
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid
@@ -0,0 +1 @@
+uid://dc3kyx3rnh13d
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc
new file mode 100644
index 0000000..f1bf7dc
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc
@@ -0,0 +1,11 @@
+:test
+ if true:
+ var value = 1
+
+ if true:
+ print(value)
+
+ print("done")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid
new file mode 100644
index 0000000..5d9dc6a
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid
@@ -0,0 +1 @@
+uid://c6pkm7n2t4wyf
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc
new file mode 100644
index 0000000..a6aa528
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc
@@ -0,0 +1,10 @@
+:test
+ ?!(1,
+ - "Option"
+ print("broken")
+
+:second
+ done
+
+:third
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid
new file mode 100644
index 0000000..13ee072
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid
@@ -0,0 +1 @@
+uid://f4q8q8et7mbo2
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc
new file mode 100644
index 0000000..51c525c
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc
@@ -0,0 +1,7 @@
+:test
+ ?!
+ - "Option"
+ print("after")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid
new file mode 100644
index 0000000..7bd0fe8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid
@@ -0,0 +1 @@
+uid://blnqmv4wjsoi8
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc
new file mode 100644
index 0000000..32f407a
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc
@@ -0,0 +1,9 @@
+:test
+ ?!
+ - "Option" [some_flag < 3
+ print("after")
+ - "Second option"
+ print("ok")
+
+:second
+ print("after_dialog")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc
new file mode 100644
index 0000000..69f88db
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc
@@ -0,0 +1,6 @@
+:talk
+ ?!
+ - UNCLE_SVEN:"Uncle Sven sends regards."
+ break
+ - "Fallback"
+ break
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid
new file mode 100644
index 0000000..7ac79a6
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid
@@ -0,0 +1 @@
+uid://doadggrcx5l6l
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc
new file mode 100644
index 0000000..0b3c59e
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc
@@ -0,0 +1,9 @@
+:talk
+ ?!("res://game/dialog_avatars/player.tres", 5, 2)
+ - "First option"
+ say($player, "First option")
+ - "Second option"
+ say($player, "Second option")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid
new file mode 100644
index 0000000..076398a
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid
@@ -0,0 +1 @@
+uid://d0xvqlf18ax11
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc
new file mode 100644
index 0000000..cdb2090
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc
@@ -0,0 +1,9 @@
+:talk
+ ?! "res://game/dialog_avatars/player.tres" 5 2
+ - "First option"
+ say($player, "First option")
+ - "Second option"
+ say($player, "Second option")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid
new file mode 100644
index 0000000..d0dd054
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid
@@ -0,0 +1 @@
+uid://cb2w2jdpxy8sr
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc
new file mode 100644
index 0000000..a36a85b
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc
@@ -0,0 +1,5 @@
+:test
+ ?!
+ - "Option"
+ done x
+ print("after")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc
new file mode 100644
index 0000000..f51f093
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc
@@ -0,0 +1,5 @@
+:test
+ global value = 1 print("oops")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid
new file mode 100644
index 0000000..bcb4fd1
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid
@@ -0,0 +1 @@
+uid://di8w10jb2c7u4
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc
new file mode 100644
index 0000000..97e2e1e
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc
@@ -0,0 +1,5 @@
+:test
+ print((1 + 2)
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid
new file mode 100644
index 0000000..a872107
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid
@@ -0,0 +1 @@
+uid://dnj6r3c3d8m5x
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc
new file mode 100644
index 0000000..0a82acd
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc
@@ -0,0 +1,41 @@
+:setup | NO_UI | NO_SAVE
+ global has_met_worker = false
+ global ask_count = 0
+ var local_count = 0
+ var target_name = $worker
+
+ if !has_met_worker and ESC_LAST_SCENE == "room1":
+ print("first visit")
+ elif $worker is active:
+ print("worker active")
+ else:
+ print("fallback")
+
+ while local_count < 3:
+ local_count = local_count + 1
+ if local_count == 2:
+ break
+
+:look "worker"
+ if $badge in inventory:
+ print("has badge")
+ elif $worker is "busy":
+ print("busy")
+ else:
+ print("idle")
+
+ ?!
+ - "Who are you?" [!has_met_worker]
+ print("ask name")
+ has_met_worker = true
+ - "Tell me more." [ask_count < 2]
+ ask_count = ask_count + 1
+ ?!
+ - "Enough."
+ break 1
+ - "I am done."
+ done
+ - "Bye."
+ break
+
+ print("after dialog")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid
new file mode 100644
index 0000000..d6713b5
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid
@@ -0,0 +1 @@
+uid://cbv6x0w8g17ap
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc
new file mode 100644
index 0000000..0c86561
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc
@@ -0,0 +1,2 @@
+:test
+ var total = $closet-door - 1
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid
new file mode 100644
index 0000000..f2e55d8
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid
@@ -0,0 +1 @@
+uid://b2mxj0r3fk4yy
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc
new file mode 100644
index 0000000..dd86b8f
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc
@@ -0,0 +1,6 @@
+:test
+ var breakfast = 1
+ var donee = 2
+ var elseify = 3
+ var notable = 4
+ print(breakfast + donee + elseify + notable)
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid
new file mode 100644
index 0000000..3bdfa1b
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid
@@ -0,0 +1 @@
+uid://d1s8r2n3kq1vh
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc
new file mode 100644
index 0000000..0203977
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc
@@ -0,0 +1,6 @@
+:test
+ done x
+ print("after")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc
new file mode 100644
index 0000000..d871c98
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc
@@ -0,0 +1,7 @@
+:test
+ if some_flag
+ print("broken")
+ print("after")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc
new file mode 100644
index 0000000..05fd749
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc
@@ -0,0 +1,5 @@
+:test
+ var x = (1 + 2
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid
new file mode 100644
index 0000000..567b162
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid
@@ -0,0 +1 @@
+uid://b2d1d5qj4o8t7
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc
new file mode 100644
index 0000000..c62d37d
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc
@@ -0,0 +1,5 @@
+:test
+ var x = 1 print("bad")
+
+:second
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid
new file mode 100644
index 0000000..f65cfb9
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid
@@ -0,0 +1 @@
+uid://dssmdd6b0cwut
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc
new file mode 100644
index 0000000..dd6ef8b
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc
@@ -0,0 +1,9 @@
+:test
+ while true
+ print("broken")
+
+:second
+ break
+
+:third
+ print("ok")
diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid
new file mode 100644
index 0000000..1a1f894
--- /dev/null
+++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid
@@ -0,0 +1 @@
+uid://dgo0jap62bmth
diff --git a/addons/escoria-core/testing/esc_new_game_state_test.gd b/addons/escoria-core/testing/esc_new_game_state_test.gd
new file mode 100644
index 0000000..7304d6a
--- /dev/null
+++ b/addons/escoria-core/testing/esc_new_game_state_test.gd
@@ -0,0 +1,38 @@
+# GdUnit generated TestSuite
+class_name ESCNewGameStateTest
+extends GdUnitTestSuite
+
+
+func test_new_game_runtime_reset_clears_transient_input_state() -> void:
+ var escoria_node = escoria.get_escoria()
+ var tool_node := ESCItem.new()
+ tool_node.global_id = "test_tool"
+ var tool_object := ESCObject.new(tool_node.global_id, tool_node)
+ var hovered_item := ESCItem.new()
+ hovered_item.global_id = "test_hovered_item"
+ hovered_item.z_index = 1
+
+ escoria.action_manager.set_current_action("use")
+ escoria.action_manager.current_tool = tool_object
+ escoria.action_manager.current_target = tool_object
+ escoria.action_manager.set_action_input_state(
+ ESCActionManager.ActionInputState.AWAITING_TARGET_ITEM
+ )
+ escoria.inputs_manager.hover_stack.hover_stack.append(hovered_item)
+ escoria.inputs_manager.hotspot_focused = "test_hotspot"
+ escoria.inputs_manager.input_mode = escoria.inputs_manager.INPUT_NONE
+
+ escoria_node._reset_new_game_runtime_state()
+
+ assert_str(escoria.action_manager.current_action).is_equal("")
+ assert_object(escoria.action_manager.current_tool).is_null()
+ assert_object(escoria.action_manager.current_target).is_null()
+ assert_int(escoria.action_manager.action_state).is_equal(
+ ESCActionManager.ActionInputState.AWAITING_VERB_OR_ITEM
+ )
+ assert_bool(escoria.inputs_manager.hover_stack.is_empty()).is_true()
+ assert_str(escoria.inputs_manager.hotspot_focused).is_equal("")
+ assert_int(escoria.inputs_manager.input_mode).is_equal(escoria.inputs_manager.INPUT_ALL)
+
+ tool_node.free()
+ hovered_item.free()
diff --git a/addons/escoria-core/testing/esc_new_game_state_test.gd.uid b/addons/escoria-core/testing/esc_new_game_state_test.gd.uid
new file mode 100644
index 0000000..a25bd5d
--- /dev/null
+++ b/addons/escoria-core/testing/esc_new_game_state_test.gd.uid
@@ -0,0 +1 @@
+uid://dce1ff86rtpla
diff --git a/addons/escoria-core/testing/esc_object_manager_test.gd b/addons/escoria-core/testing/esc_object_manager_test.gd
new file mode 100644
index 0000000..52f79b3
--- /dev/null
+++ b/addons/escoria-core/testing/esc_object_manager_test.gd
@@ -0,0 +1,79 @@
+# GdUnit generated TestSuite
+class_name ESCObjectManagerTest
+extends GdUnitTestSuite
+
+
+func test_reset_game_state_clears_persisted_room_object_state() -> void:
+ var object_manager := ESCObjectManager.new()
+ var room := ESCRoom.new()
+ room.global_id = "test_room"
+ object_manager.set_current_room(room)
+
+ var first_item := ESCItem.new()
+ first_item.global_id = "test_item"
+ var first_object := ESCObject.new(first_item.global_id, first_item)
+ object_manager.register_object(first_object, room, false, false)
+ first_object.set_state("opened")
+
+ var room_key := ESCRoomObjectsKey.new()
+ room_key.room_global_id = room.global_id
+ room_key.room_instance_id = room.get_instance_id()
+ object_manager.unregister_object(first_object, room_key)
+ assert_bool(object_manager.has(first_item.global_id, room)).is_true()
+
+ object_manager.reset_game_state()
+ assert_bool(object_manager.has(first_item.global_id, room)).is_false()
+
+ var second_item := ESCItem.new()
+ second_item.global_id = "test_item"
+ var second_object := ESCObject.new(second_item.global_id, second_item)
+ object_manager.set_current_room(room)
+ object_manager.register_object(second_object, room, false, false)
+
+ assert_str(object_manager.get_object(second_item.global_id, room).state).is_equal(
+ ESCObject.STATE_DEFAULT
+ )
+
+ first_item.free()
+ second_item.free()
+ room.free()
+
+
+func test_reset_game_state_preserves_reserved_objects() -> void:
+ var object_manager := ESCObjectManager.new()
+ var music_node := Node.new()
+
+ object_manager.register_object(
+ ESCObject.new(ESCObjectManager.MUSIC, music_node),
+ null,
+ true,
+ false
+ )
+
+ object_manager.reset_game_state()
+
+ assert_bool(object_manager.has(ESCObjectManager.MUSIC)).is_true()
+ assert_object(object_manager.get_object(ESCObjectManager.MUSIC).node).is_same(music_node)
+
+ music_node.free()
+
+
+func test_reset_game_state_clears_saved_terrain_state() -> void:
+ var object_manager := ESCObjectManager.new()
+ var room := ESCRoom.new()
+ room.global_id = "test_room"
+ var navigation_region := NavigationRegion2D.new()
+
+ object_manager.set_current_room(room)
+ object_manager.register_terrain(
+ ESCObject.new("test_terrain", navigation_region),
+ room
+ )
+ assert_int(object_manager.room_terrains.size()).is_equal(1)
+
+ object_manager.reset_game_state()
+
+ assert_int(object_manager.room_terrains.size()).is_equal(0)
+
+ navigation_region.free()
+ room.free()
diff --git a/addons/escoria-core/testing/esc_object_manager_test.gd.uid b/addons/escoria-core/testing/esc_object_manager_test.gd.uid
new file mode 100644
index 0000000..55ec250
--- /dev/null
+++ b/addons/escoria-core/testing/esc_object_manager_test.gd.uid
@@ -0,0 +1 @@
+uid://b5f3fwnatn16n
diff --git a/addons/escoria-core/testing/issue_946_room_transition_test.gd b/addons/escoria-core/testing/issue_946_room_transition_test.gd
new file mode 100644
index 0000000..937f441
--- /dev/null
+++ b/addons/escoria-core/testing/issue_946_room_transition_test.gd
@@ -0,0 +1,104 @@
+class_name Issue946RoomTransitionTest
+extends GdUnitTestSuite
+
+
+const MARK_SCRIPT := "res://game/characters/mark/mark.esc"
+const ROOM_05 := "res://game/rooms/room05/room05.tscn"
+const ROOM_06 := "res://game/rooms/room06/room06.tscn"
+const EVENT_FINISHED_TIMEOUT_SECONDS := 5.0
+
+
+var _compiler_before: ESCCompiler
+var _event_finished_rc: Variant = []
+var _event_finished_timed_out := false
+
+
+class CountingCompiler extends ESCCompiler:
+ var load_counts: Dictionary = {}
+
+
+ func load_esc_file(path: String, associated_global_id: String = "") -> ESCScript:
+ load_counts[path] = load_counts.get(path, 0) + 1
+ return super.load_esc_file(path, associated_global_id)
+
+
+ func get_load_count(path: String) -> int:
+ return load_counts.get(path, 0)
+
+
+ func reset_counts() -> void:
+ load_counts.clear()
+
+
+func before() -> void:
+ _compiler_before = escoria.esc_compiler
+ escoria.esc_compiler = CountingCompiler.new()
+ escoria.event_manager.interrupt([], false)
+ escoria.event_manager.clear_event_queue()
+
+
+func after() -> void:
+ escoria.event_manager.interrupt([], false)
+ escoria.event_manager.clear_event_queue()
+ escoria.esc_compiler = _compiler_before
+
+
+func test_room_6_to_room_5_loads_player_script_once() -> void:
+ var compiler := escoria.esc_compiler as CountingCompiler
+
+ await _change_scene_and_wait(ROOM_06, false)
+
+ compiler.reset_counts()
+
+ await _change_scene_and_wait(ROOM_05, true)
+ await _wait_for_frames(3)
+
+ assert_object(escoria.object_manager.get_object("player")).is_not_null()
+ assert_int(compiler.get_load_count(MARK_SCRIPT)).is_equal(1)
+
+
+func _change_scene_and_wait(room_path: String, enable_automatic_transition: bool) -> void:
+ var room_ready: Signal = escoria.main.room_ready
+ ChangeSceneCommand.new().run([room_path, enable_automatic_transition])
+ await room_ready
+ if enable_automatic_transition:
+ await _wait_for_event_finished(escoria.event_manager.EVENT_TRANSITION_IN)
+
+
+func _wait_for_event_finished(event_name: String) -> void:
+ _event_finished_rc = []
+ _event_finished_timed_out = false
+
+ escoria.event_manager.event_finished.connect(_on_wait_event_finished)
+ var timeout_timer := get_tree().create_timer(EVENT_FINISHED_TIMEOUT_SECONDS)
+ timeout_timer.timeout.connect(_on_wait_event_finished_timeout)
+
+ while not _event_finished_timed_out:
+ await get_tree().process_frame
+ var rc := _event_finished_rc
+ if rc is Array and rc.size() > 1 and rc[1] == event_name:
+ escoria.event_manager.event_finished.disconnect(_on_wait_event_finished)
+ if timeout_timer.timeout.is_connected(_on_wait_event_finished_timeout):
+ timeout_timer.timeout.disconnect(_on_wait_event_finished_timeout)
+ return
+
+ if escoria.event_manager.event_finished.is_connected(_on_wait_event_finished):
+ escoria.event_manager.event_finished.disconnect(_on_wait_event_finished)
+
+ fail(
+ "Timed out waiting for escoria.event_manager.event_finished for event '%s'."
+ % event_name
+ )
+
+
+func _on_wait_event_finished(return_code, finished_event_name) -> void:
+ _event_finished_rc = [return_code, finished_event_name]
+
+
+func _on_wait_event_finished_timeout() -> void:
+ _event_finished_timed_out = true
+
+
+func _wait_for_frames(count: int) -> void:
+ for frame in count:
+ await get_tree().process_frame
diff --git a/addons/escoria-core/testing/issue_946_room_transition_test.gd.uid b/addons/escoria-core/testing/issue_946_room_transition_test.gd.uid
new file mode 100644
index 0000000..5af4b20
--- /dev/null
+++ b/addons/escoria-core/testing/issue_946_room_transition_test.gd.uid
@@ -0,0 +1 @@
+uid://dd4msfbyuho46
diff --git a/addons/escoria-core/testing/player_angles_finder.gd b/addons/escoria-core/testing/player_angles_finder.gd
index e47b83c..3c2c48a 100644
--- a/addons/escoria-core/testing/player_angles_finder.gd
+++ b/addons/escoria-core/testing/player_angles_finder.gd
@@ -8,11 +8,7 @@ extends Node2D
# Clicking under his feet is angle 180.
# etc.
-var number_of_directions: int
-var angle_horizontal_axes: float
-var angle_vertical_axes: float
-var angle_diagonal_axes: float
-const POLYGON_DISTANCE = 400
+
enum Directions {
NORTH, # 0
@@ -25,7 +21,9 @@ enum Directions {
NORTHWEST # 7
}
-const starting_angles = [
+const POLYGON_DISTANCE = 400
+
+const STARTING_ANGLES = [
0, # 0 NORTH
PI/4, # 1 NORTHEAST
PI/2, # 2 EAST
@@ -36,6 +34,12 @@ const starting_angles = [
7*PI/4, # 7 NORTHWEST
]
+var number_of_directions: int
+var angle_horizontal_axes: float
+var angle_vertical_axes: float
+var angle_diagonal_axes: float
+
+
var colors = [
Color("red"), # 0 NORTH
Color("green"), # 1 NORTHEAST
@@ -153,7 +157,7 @@ func calculate_areas(nb_directions: int = 8):
# Then minus angle_area/2 to align on direction
start_angle = PI/2 + angle_area / 2
- var angle_start = starting_angles[i] - start_angle
+ var angle_start = STARTING_ANGLES[i] - start_angle
var angle_end = angle_start + angle_area
angles.push_back([angle_start, angle_end])
@@ -217,7 +221,7 @@ func _on_angle_diag_text_changed(new_text: String):
calculate_areas()
-func _on_area_click(viewport: Object, event: InputEvent, shape_idx: int, area_name: String):
+func _on_area_click(_viewport: Object, event: InputEvent, _shape_idx: int, _area_name: String):
if event is InputEventMouseButton and event.is_pressed():
pass
diff --git a/addons/escoria-core/testing/rtl_screen_offset_testing.gd b/addons/escoria-core/testing/rtl_screen_offset_testing.gd
index 4083bbe..87de0bc 100644
--- a/addons/escoria-core/testing/rtl_screen_offset_testing.gd
+++ b/addons/escoria-core/testing/rtl_screen_offset_testing.gd
@@ -1,17 +1,20 @@
extends Control
-@onready var screen_width = get_tree().get_root().get_viewport().size.x
-@onready var screen_height = get_tree().get_root().get_viewport().size.y
-var global_distance_to_clamp = 40: get = get_global_dist_clamp, set = set_global_dist_clamp
-
signal mouse_moved(position)
signal text_selected(text)
-@export var path_to_richtextlabel: NodePath
-const ONE_LINE_HEIGHT = 16
-@export var max_width: int = 200
const MIN_HEIGHT = 30
const MAX_HEIGHT = 500
+const ONE_LINE_HEIGHT = 16
+
+@export var path_to_richtextlabel: NodePath
+@export var max_width: int = 200
+
+var global_distance_to_clamp = 40: get = get_global_dist_clamp, set = set_global_dist_clamp
+
+@onready var screen_width = get_tree().get_root().get_viewport().size.x
+@onready var screen_height = get_tree().get_root().get_viewport().size.y
+
func _ready():
assert(!path_to_richtextlabel.is_empty())
diff --git a/addons/escoria-core/tools/logging/esc_logger.gd b/addons/escoria-core/tools/logging/esc_logger.gd
index 26d8d86..52bfcde 100644
--- a/addons/escoria-core/tools/logging/esc_logger.gd
+++ b/addons/escoria-core/tools/logging/esc_logger.gd
@@ -21,14 +21,13 @@ class ESCLoggerBase:
## Log filename format
const LOG_FILE_FORMAT: String = "log_%s_%s.log"
- # Configured log level
- var _log_level: int
-
## If true, assert() functions will not be called, thus the program won't
## exit or error. Resets to false after an assert() call was ignored once.
## Useful for console calls.
var dont_assert: bool = false
+ # Configured log level
+ var _log_level: int
# Constructor
func _init():
diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd
index 3ed0025..038d3f2 100644
--- a/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd
+++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_exit_scene_script_analyzer.gd
@@ -1,5 +1,5 @@
-extends ESCScriptAnalyzer
class_name ESCExitSceneScriptAnalyzer
+extends ESCScriptAnalyzer
const EXIT_SCENE_EVENT_NAME = "exit_scene"
@@ -18,9 +18,9 @@ const BULLET_CHARACTER = "- "
# These are vars because they have to be, but should be treated as consts
-var TRANSITION_COMMAND_NAME = TransitionCommand.new().get_command_name()
-var CHANGE_SCENE_COMMAND_NAME = ChangeSceneCommand.new().get_command_name()
-var ACCEPT_INPUT_COMMAND_NAME = AcceptInputCommand.new().get_command_name()
+var transition_command_name = TransitionCommand.new().get_command_name()
+var change_scene_command_name = ChangeSceneCommand.new().get_command_name()
+var accept_input_command_name = AcceptInputCommand.new().get_command_name()
var _has_change_scene_command: bool = false
@@ -96,7 +96,7 @@ func visit_call_expr(expr: ESCGrammarExprs.Call):
_has_commands_after_change_scene_command = _has_change_scene_command_in_scope()
- if command_name == CHANGE_SCENE_COMMAND_NAME:
+ if command_name == change_scene_command_name:
_has_change_scene_command = true
_change_scene_token = expr.get_paren_token()
@@ -109,9 +109,9 @@ func visit_call_expr(expr: ESCGrammarExprs.Call):
if is_auto_transition != null and not is_auto_transition:
_check_for_missing_accept_input_disable()
- elif command_name == TRANSITION_COMMAND_NAME:
+ elif command_name == transition_command_name:
_check_for_missing_accept_input_disable()
- elif command_name == ACCEPT_INPUT_COMMAND_NAME:
+ elif command_name == accept_input_command_name:
var first_arg = args[0] as String
if first_arg != null and first_arg.to_lower() in ACCEPT_INPUT_DISABLE_ARGS:
diff --git a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd
index 874ffa1..2771e43 100644
--- a/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd
+++ b/addons/escoria-core/tools/script_analysis/analyzers/esc_script_analyzer.gd
@@ -1,5 +1,5 @@
-extends RefCounted
class_name ESCScriptAnalyzer
+extends RefCounted
const CURRENT_PLAYER_KEYWORD = "CURRENT_PLAYER"
@@ -18,7 +18,7 @@ var _builtin_functions: Array = [
# This must be implemented in child class.
-func analyze(statements: Array) -> void:
+func analyze(_statements: Array) -> void:
pass
@@ -75,8 +75,7 @@ func visit_call_expr(expr: ESCGrammarExprs.Call):
if not callee is ESCBaseCommand:
if not callee in _builtin_functions:
return 0
- else:
- return _handle_builtin_function(callee, args)
+ return _handle_builtin_function(callee, args)
return ESCExecution.RC_OK
@@ -115,7 +114,7 @@ func visit_while_stmt(stmt: ESCGrammarStmts.While):
return null
-func visit_pass_stmt(stmt: ESCGrammarStmts.Pass):
+func visit_pass_stmt(_stmt: ESCGrammarStmts.Pass):
pass
@@ -147,11 +146,11 @@ func visit_global_stmt(stmt: ESCGrammarStmts.Global):
return null
-func visit_dialog_stmt(stmt: ESCGrammarStmts.Dialog):
+func visit_dialog_stmt(_stmt: ESCGrammarStmts.Dialog):
return null
-func visit_dialog_option_stmt(stmt: ESCGrammarStmts.DialogOption):
+func visit_dialog_option_stmt(_stmt: ESCGrammarStmts.DialogOption):
pass
@@ -295,7 +294,7 @@ func _look_up_object(name: ESCToken):
# We have no way of knowing what the value will actually be since this is called during static
# analysis. As such, we'll just return null.
-func _look_up_object_by_global_id(global_id: String):
+func _look_up_object_by_global_id(_global_id: String):
return null
@@ -305,8 +304,7 @@ 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())
func _evaluate(expr: ESCGrammarExpr):
diff --git a/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd
index 2447a87..f074e6e 100644
--- a/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd
+++ b/addons/escoria-core/tools/script_analysis/esc_ashes_analyzer.gd
@@ -1,6 +1,6 @@
@tool
-extends Node
class_name ESCAshesAnalyzer
+extends Node
# TODO: Update this when we move from the .esc extension for Escoria scripts to .ash
diff --git a/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd
index 53aa7a9..b06bd78 100644
--- a/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd
+++ b/addons/escoria-core/tools/script_analysis/esc_static_analyzers.gd
@@ -1,5 +1,5 @@
-extends RefCounted
class_name ESCStaticAnalyzers
+extends RefCounted
var _analyzers = [
diff --git a/addons/escoria-core/ui_library/fonts/caslonantique.ttf b/addons/escoria-core/ui_library/fonts/caslonantique.ttf
index eec6f63..eec6f63 100644..100755
--- a/addons/escoria-core/ui_library/fonts/caslonantique.ttf
+++ b/addons/escoria-core/ui_library/fonts/caslonantique.ttf
Binary files differ
diff --git a/addons/escoria-core/ui_library/inventory/esc_inventory_button.gd b/addons/escoria-core/ui_library/inventory/esc_inventory_button.gd
index b109c38..390055f 100644
--- a/addons/escoria-core/ui_library/inventory/esc_inventory_button.gd
+++ b/addons/escoria-core/ui_library/inventory/esc_inventory_button.gd
@@ -1,7 +1,7 @@
## The inventory representation of an ESC item if pickable (only used by
## the inventory components)
-extends TextureButton
class_name ESCInventoryButton
+extends TextureButton
## Signal emitted when the item was left clicked.[br]
## [br]
diff --git a/addons/escoria-core/ui_library/inventory/esc_inventory_container.gd b/addons/escoria-core/ui_library/inventory/esc_inventory_container.gd
index 1e6dab8..748d714 100644
--- a/addons/escoria-core/ui_library/inventory/esc_inventory_container.gd
+++ b/addons/escoria-core/ui_library/inventory/esc_inventory_container.gd
@@ -1,7 +1,7 @@
## Inventory container handler that acts as a base for UIs inventory containers.
-extends Control
class_name ESCInventoryContainer
+extends Control
## Whether any item in the container is currently focused.[br]
var item_focused: bool = false
@@ -85,7 +85,7 @@ func get_inventory_button(inventory_item: ESCInventoryItem) -> ESCInventoryButto
## #### Returns[br]
## [br]
## Returns nothing.
-func _on_inventory_item_focused(item_id):
+func _on_inventory_item_focused(_item_id):
item_focused = true
diff --git a/addons/escoria-core/ui_library/menus/load_save/save/save_game.gd b/addons/escoria-core/ui_library/menus/load_save/save/save_game.gd
index 33bbb13..ad7e1ca 100644
--- a/addons/escoria-core/ui_library/menus/load_save/save/save_game.gd
+++ b/addons/escoria-core/ui_library/menus/load_save/save/save_game.gd
@@ -29,9 +29,10 @@ func _on_slot_pressed(p_slot_n: int):
# Create the slots from the list of savegames
func refresh_savegames():
- var _slots = $VBoxContainer/ScrollContainer/slots
- for slot in _slots.get_children():
- _slots.remove_child(slot)
+ var slots = $VBoxContainer/ScrollContainer/slots
+ for slot in slots.get_children():
+ slots.remove_child(slot)
+ slot.queue_free()
var saves_list = escoria.save_manager.get_saves_list()
@@ -44,8 +45,8 @@ func refresh_savegames():
datetime["minute"],
]
var new_slot = slot_ui_scene.instantiate()
- _slots.add_child(new_slot)
- _slots.move_child(new_slot, 0)
+ slots.add_child(new_slot)
+ slots.move_child(new_slot, 0)
new_slot.set_slot_name_date(tr("New save"), datetime_string)
new_slot.connect("pressed", Callable(self, "_on_slot_pressed").bind(saves_list.size()+1))
@@ -56,7 +57,7 @@ func refresh_savegames():
for save in saves_array:
new_slot = slot_ui_scene.instantiate()
- _slots.add_child(new_slot)
+ slots.add_child(new_slot)
datetime_string = "%02d/%02d/%02d %02d:%02d" % [
save.date["day"],
diff --git a/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd b/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd
index b631ef2..00ac81f 100644
--- a/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd
+++ b/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd
@@ -19,3 +19,7 @@ func _input(event):
and event.keycode == KEY_ENTER \
or event.keycode == KEY_KP_ENTER:
_on_ok_pressed()
+
+
+func _on_about_to_popup() -> void:
+ $MarginContainer/VBoxContainer/LineEdit.grab_focus()
diff --git a/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.tscn b/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.tscn
index 8d6fc57..5bfb606 100644
--- a/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.tscn
+++ b/addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.tscn
@@ -1,71 +1,46 @@
-[gd_scene load_steps=2 format=3 uid="uid://coiqm0xcq8f8o"]
+[gd_scene format=3 uid="uid://coiqm0xcq8f8o"]
[ext_resource type="Script" uid="uid://blqbjhwtenth5" path="res://addons/escoria-core/ui_library/menus/load_save/save/save_name_popup.gd" id="2"]
-[node name="save_name_popup" type="Popup"]
-anchor_right = 1.0
-anchor_bottom = 1.0
+[node name="save_name_popup" type="Popup" unique_id=1050292675]
+oversampling_override = 1.0
+size = Vector2i(209, 185)
script = ExtResource("2")
-__meta__ = {
-"_edit_use_anchors_": false
-}
-[node name="MarginContainer" type="MarginContainer" parent="."]
+[node name="MarginContainer" type="MarginContainer" parent="." unique_id=410184148]
+anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
-theme_override_constants/margin_right = 30
-theme_override_constants/margin_top = 30
theme_override_constants/margin_left = 30
+theme_override_constants/margin_top = 30
+theme_override_constants/margin_right = 30
theme_override_constants/margin_bottom = 30
-__meta__ = {
-"_edit_use_anchors_": false
-}
-[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
-offset_left = 30.0
-offset_top = 401.0
-offset_right = 1250.0
-offset_bottom = 499.0
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=440596471]
+layout_mode = 2
size_flags_vertical = 4
theme_override_constants/separation = 20
-[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
-offset_right = 1220.0
-offset_bottom = 14.0
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer" unique_id=2007386692]
+layout_mode = 2
text = "ENTER_SAVE_NAME"
-[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer"]
-offset_top = 34.0
-offset_right = 1220.0
-offset_bottom = 58.0
+[node name="LineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer" unique_id=1605064251]
+layout_mode = 2
-[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
-offset_top = 78.0
-offset_right = 1220.0
-offset_bottom = 98.0
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=1175843435]
+layout_mode = 2
theme_override_constants/separation = 10
alignment = 2
-__meta__ = {
-"_edit_use_anchors_": false
-}
-[node name="cancel" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
-offset_left = 1118.0
-offset_right = 1179.0
-offset_bottom = 20.0
+[node name="cancel" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=1190606511]
+layout_mode = 2
text = "CANCEL"
-__meta__ = {
-"_edit_use_anchors_": false
-}
-[node name="ok" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
-offset_left = 1189.0
-offset_right = 1220.0
-offset_bottom = 20.0
+[node name="ok" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=2090364237]
+layout_mode = 2
text = "OK"
-__meta__ = {
-"_edit_use_anchors_": false
-}
+[connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/ok" to="." method="_on_ok_pressed"]
diff --git a/addons/escoria-core/ui_library/menus/options/options.gd b/addons/escoria-core/ui_library/menus/options/options.gd
index 32a9758..7b270fc 100644
--- a/addons/escoria-core/ui_library/menus/options/options.gd
+++ b/addons/escoria-core/ui_library/menus/options/options.gd
@@ -24,24 +24,24 @@ var _loaded_languages: Array = []
# Initialize the flags
func _ready() -> void:
- var _flags_container: HBoxContainer = \
+ var flags_container: HBoxContainer = \
$VBoxContainer/MarginContainer/options/flags
- for child in _flags_container.get_children():
- _flags_container.remove_child(child)
+ for child in flags_container.get_children():
+ flags_container.remove_child(child)
child.queue_free()
_loaded_languages = []
- for lang in TranslationServer.get_loaded_locales():
- if not lang in _loaded_languages:
- _loaded_languages.append(lang)
- var _lang = TextureRect.new()
- _lang.texture = load(
+ for language in TranslationServer.get_loaded_locales():
+ if not language in _loaded_languages:
+ _loaded_languages.append(language)
+ var language_item = TextureRect.new()
+ language_item.texture = load(
"res://addons/escoria-core/ui_library" + \
- "/menus/options/flags/%s.png" % lang
+ "/menus/options/flags/%s.png" % language
)
- _flags_container.add_child(_lang)
- _lang.connect("gui_input", Callable(self, "_on_language_input").bind(lang))
+ flags_container.add_child(language_item)
+ language_item.connect("gui_input", Callable(self, "_on_language_input").bind(language))
# Show the options
@@ -56,13 +56,13 @@ func show():
# #### Parameters
# - p_settings: The settings to use
func initialize_options(p_settings):
- var _options = $VBoxContainer/MarginContainer/options
- _options.get_node("general_volume").value = p_settings["master_volume"]
- _options.get_node("sound_volume").value = p_settings["sfx_volume"]
- _options.get_node("music_volume").value = p_settings["music_volume"]
- _options.get_node("speech_volume").value = p_settings["speech_volume"]
- _options.get_node("ambient_volume").value = p_settings["ambient_volume"]
- _options.get_node("fullscreen").set_pressed_no_signal(p_settings["fullscreen"])
+ var options = $VBoxContainer/MarginContainer/options
+ options.get_node("general_volume").value = p_settings["master_volume"]
+ options.get_node("sound_volume").value = p_settings["sfx_volume"]
+ options.get_node("music_volume").value = p_settings["music_volume"]
+ options.get_node("speech_volume").value = p_settings["speech_volume"]
+ options.get_node("ambient_volume").value = p_settings["ambient_volume"]
+ options.get_node("fullscreen").set_pressed_no_signal(p_settings["fullscreen"])
# The language was changed