From 895700870d4d3b4da305d470bb7390762d8382db Mon Sep 17 00:00:00 2001 From: Duncan Brown Date: Sat, 27 Aug 2022 14:08:57 -0400 Subject: [PATCH] feat: allows for default/fallback actions to be specified --- action_defaults.esc | 2 + .../core-scripts/esc/esc_action_manager.gd | 268 +++++++++--------- addons/escoria-core/game/esc_autoload.gd | 4 + .../game/esc_project_settings_manager.gd | 1 + addons/escoria-core/game/escoria.gd | 16 ++ addons/escoria-core/plugin.gd | 10 + project.godot | 1 + 7 files changed, 170 insertions(+), 132 deletions(-) create mode 100644 action_defaults.esc diff --git a/action_defaults.esc b/action_defaults.esc new file mode 100644 index 00000000..95bdde09 --- /dev/null +++ b/action_defaults.esc @@ -0,0 +1,2 @@ +:use | TK +say player "I can't use this!" 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 cc0dc1ac..7cd8c770 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 @@ -230,27 +230,36 @@ func clear_current_tool(): set_action_input_state(ACTION_INPUT_STATE.AWAITING_ITEM) -# Activates the action for given params +# 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. # # #### Parameters # -# - action String Action to execute (defined in attached ESC file and in +# - action: Action to execute (defined in attached ESC file and in # action verbs UI) eg: arrived, use, look, pickup... # - target: Target ESC object # - combine_with: ESC object to combine with -func _activate( +# +# *Returns* the appropriate ESCEvent to queue/run, or null if none can be found +# or there's a reason not to run an event. +func _get_event_to_queue( action: String, target: ESCObject, combine_with: ESCObject = null -) -> int: +) -> ESCEvent: + escoria.logger.info( self, - "Activated action %s on %s." % [action, target] + "Checking action %s on %s..." % [action, target] ) + var event_to_return: ESCEvent = null + # If we're using an action which item requires to combine - if target.node is ESCItem\ + if target.node is ESCItem \ and action in target.node.combine_when_selected_action_is_in: + # Check if object must be in inventory to be used if target.node.use_from_inventory_only: if escoria.inventory_manager.inventory_has(target.global_id): @@ -273,64 +282,39 @@ func _activate( action, target.global_id ] - if target.events.has(target_event): - escoria.event_manager.queue_event(target.events[ - target_event - ]) - var event_returned = yield( - escoria.event_manager, - "event_finished" - ) - while event_returned[1] != target_event: - event_returned = yield( - escoria.event_manager, - "event_finished" - ) - clear_current_action() - emit_signal("action_finished") - return event_returned[0] + if target.events.has(target_event): + event_to_return = target.events[target_event] elif combine_with.events.has(combine_with_event)\ and not combine_with.node.combine_is_one_way: - escoria.event_manager.queue_event( - combine_with.events[ - combine_with_event - ] - ) - var event_returned = yield( - escoria.event_manager, - "event_finished" - ) - while event_returned[1] != combine_with_event: - event_returned = yield( - escoria.event_manager, - "event_finished" - ) - clear_current_action() - emit_signal("action_finished") - return event_returned[0] + event_to_return = combine_with.events[combine_with_event] else: - var errors = [ - "Attempted to execute action %s between item %s and item %s" % [ - action, - target.global_id, - combine_with.global_id - ] - ] - if combine_with.node.combine_is_one_way: - errors.append( - ("Reason: %s's item interaction " +\ - "is one-way.") % combine_with.global_id - ) - escoria.logger.warn( - self, - "Invalid action" + str(errors) - ) + # 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): - clear_current_action() - emit_signal("action_finished") - return ESCExecution.RC_ERROR + event_to_return = escoria.action_default_script.events[action] + else: + var errors = [ + "Attempted to execute action %s between item %s and item %s" % [ + action, + target.global_id, + combine_with.global_id + ] + ] + + if combine_with.node.combine_is_one_way: + errors.append( + ("Reason: %s's item interaction " + \ + "is one-way.") % combine_with.global_id + ) + + escoria.logger.warn( + self, + "Invalid action: " + str(errors) + ) else: escoria.logger.warn( self, @@ -344,17 +328,6 @@ func _activate( combine_with.global_id ] ) - - clear_current_action() - emit_signal("action_finished") - return ESCExecution.RC_ERROR - else: - # We're missing a target here for our tool to be used on - current_tool = target - set_action_input_state( - ACTION_INPUT_STATE.AWAITING_TARGET_ITEM - ) - return ESCExecution.RC_OK else: escoria.logger.warn( self, @@ -366,33 +339,45 @@ func _activate( ] + "but item must be in inventory." ) + else: + if target.events.has(action): + event_to_return = target.events[action] + elif escoria.action_default_script \ + and escoria.action_default_script.events.has(action): - if target.events.has(action): - escoria.event_manager.queue_event(target.events[action]) - var event_returned = yield( + # If there's a "fallback" action to run, return it + event_to_return = escoria.action_default_script.events[action] + else: + escoria.logger.warn( + self, + "Invalid action: " + + "Event for action %s on object %s not found." % [ + action, + target.global_id + ] + ) + + return event_to_return + + +func _run_event(event: ESCEvent) -> int: + escoria.event_manager.queue_event(event) + + var event_returned = yield( + escoria.event_manager, + "event_finished" + ) + + while event_returned[1] != event.name: + event_returned = yield( escoria.event_manager, "event_finished" ) - while event_returned[1] != action: - event_returned = yield( - escoria.event_manager, - "event_finished" - ) - clear_current_action() - emit_signal("action_finished") - return event_returned[0] - else: - escoria.logger.warn( - self, - "Invalid action: " + - "Event for action %s on object %s not found." % [ - action, - target.global_id - ] - ) - clear_current_action() - emit_signal("action_finished") - return ESCExecution.RC_ERROR + + clear_current_action() + emit_signal("action_finished") + + return event_returned[0] # Makes an object walk to a destination. This can be either a 2D position or @@ -460,6 +445,8 @@ func perform_inputevent_on_object( ): """ 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 @@ -485,12 +472,6 @@ func perform_inputevent_on_object( "%s to perform event %s." % [obj.global_id, event] ) - var event_flags = 0 - var has_current_action: bool = false - if obj.events.has(current_action): - event_flags = obj.events[current_action].flags - has_current_action = true - # Don't interact after player movement towards object # (because object is inactive for example) var dont_interact = false @@ -501,11 +482,11 @@ func perform_inputevent_on_object( var tool_just_set = _set_tool_and_action(obj, default_action) var need_combine = _check_item_needs_combine() - # If the current tool was not set, this is our first item, make it tool + # If the current tool was not set, this is our first item, make it the tool if not current_tool or (current_tool and not need_combine): current_tool = obj - # Else, if we have a tool an combination required, this is our second item, - # make it target. + # Else, if we have a tool and combination required, this is our second item, + # make it the target. elif need_combine and not tool_just_set: current_target = obj @@ -518,19 +499,68 @@ func perform_inputevent_on_object( 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) + var event_to_queue: ESCEvent = null + + # Manage exits + if obj.node.is_exit and current_action in ["", ACTION_WALK]: + event_to_queue = _get_event_to_queue(ACTION_EXIT_SCENE, obj) + else: + # Manage movements towards object before activating it + if 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) + # Manage action on object + elif not current_action in ["", ACTION_WALK]: + if need_combine and current_target: + event_to_queue = _get_event_to_queue( + current_action, + current_tool, + current_target + ) + else: + # Check if object must be in inventory to be used and update + # action state if necessary + if obj.node.use_from_inventory_only and \ + escoria.inventory_manager.inventory_has(obj.global_id): + + # We're missing a target here for our tool to be used on + current_tool = obj + set_action_input_state( + ACTION_INPUT_STATE.AWAITING_TARGET_ITEM + ) + + # We need to wait for that target + return + else: + event_to_queue = _get_event_to_queue( + current_action, + obj + ) + + # Get out of here if there's a specified action but an event couldn't be found. + # Note that `event_to_queue` may still be null, but we do need to start the + # player walking towards the destination. + if current_action and not event_to_queue: + clear_current_action() + emit_signal("action_finished") + return + + var event_flags = event_to_queue.flags if event_to_queue else 0 + if escoria.main.current_scene.player: - var destination_position: Vector2 = escoria.main.current_scene.player.\ - global_position + var destination_position: Vector2 = escoria.main.current_scene.player \ + .global_position # If clicked object not in inventory, player walks towards it if not obj.node is ESCPlayer and \ not escoria.inventory_manager.inventory_has(obj.global_id) and \ - (not has_current_action or not event_flags & ESCEvent.FLAG_TK): + not event_flags & ESCEvent.FLAG_TK: var context = _walk_towards_object( obj, event.position, event.doubleclick ) + if context is GDScriptFunctionState: context = yield(context, "completed") @@ -562,36 +592,10 @@ func perform_inputevent_on_object( [escoria.event_manager.EVENT_CANT_REACH, obj.global_id] ) - # If no interaction should happen after player has arrived, leave # immediately. - if dont_interact: - return - - # Manage exits - if obj.node.is_exit and current_action in ["", ACTION_WALK]: - escoria.event_manager.interrupt() - escoria.event_manager.clear_event_queue() - _activate(ACTION_EXIT_SCENE, obj) - else: - # Manage movements towards object before activating it - if current_action in ["", ACTION_WALK] and \ - not escoria.inventory_manager.inventory_has(obj.global_id): - _activate(ACTION_ARRIVED, obj) - # Manage action on object - elif not current_action in ["", ACTION_WALK]: - if need_combine and current_target: - _activate( - current_action, - current_tool, - current_target - ) - - else: - _activate( - current_action, - obj - ) + if not dont_interact and event_to_queue: + _run_event(event_to_queue) # Determines whether the object in question can be acted upon. diff --git a/addons/escoria-core/game/esc_autoload.gd b/addons/escoria-core/game/esc_autoload.gd index 56a07706..c2e4f8b6 100644 --- a/addons/escoria-core/game/esc_autoload.gd +++ b/addons/escoria-core/game/esc_autoload.gd @@ -97,6 +97,10 @@ var player_camera: ESCCamera # escoria/main/game_start_script var start_script: ESCScript +# The "fallback" script to use when an action is tried on an item that hasn't +# been explicitly scripted. +var action_default_script: ESCScript + # Whether we ran a room directly from editor, not a full game var is_direct_room_run: bool = false diff --git a/addons/escoria-core/game/esc_project_settings_manager.gd b/addons/escoria-core/game/esc_project_settings_manager.gd index 0d332d9f..1668eaa3 100644 --- a/addons/escoria-core/game/esc_project_settings_manager.gd +++ b/addons/escoria-core/game/esc_project_settings_manager.gd @@ -26,6 +26,7 @@ const FORCE_QUIT = "%s/%s/force_quit" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const GAME_MIGRATION_PATH = "%s/%s/game_migration_path" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const GAME_VERSION = "%s/%s/game_version" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const GAME_START_SCRIPT = "%s/%s/game_start_script" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] +const ACTION_DEFAULT_SCRIPT = "%s/%s/action_default_script" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const SAVEGAMES_PATH = "%s/%s/savegames_path" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const SETTINGS_PATH = "%s/%s/settings_path" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] const TEXT_LANG = "%s/%s/text_lang" % [_ESCORIA_SETTINGS_ROOT, _MAIN_ROOT] diff --git a/addons/escoria-core/game/escoria.gd b/addons/escoria-core/game/escoria.gd index 6c4556c0..b67c1875 100644 --- a/addons/escoria-core/game/escoria.gd +++ b/addons/escoria-core/game/escoria.gd @@ -52,6 +52,7 @@ func _ready(): escoria.room_manager.register_reserved_globals() escoria.inputs_manager.register_core() + if ESCProjectSettingsManager.get_setting( ESCProjectSettingsManager.GAME_START_SCRIPT ).empty(): @@ -66,6 +67,21 @@ func _ready(): ) ) + if ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.ACTION_DEFAULT_SCRIPT + ).empty(): + escoria.logger.info( + self, + "Project setting '%s' is not set. No action defaults will be used." + % ESCProjectSettingsManager.ACTION_DEFAULT_SCRIPT + ) + else: + escoria.action_default_script = escoria.esc_compiler.load_esc_file( + ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.ACTION_DEFAULT_SCRIPT + ) + ) + escoria.main = main _perform_plugins_checks() diff --git a/addons/escoria-core/plugin.gd b/addons/escoria-core/plugin.gd index 3eb7fbb2..01dd598d 100644 --- a/addons/escoria-core/plugin.gd +++ b/addons/escoria-core/plugin.gd @@ -156,6 +156,16 @@ func set_escoria_main_settings(): } ) + register_setting( + ESCProjectSettingsManager.ACTION_DEFAULT_SCRIPT, + "", + { + "type": TYPE_STRING, + "hint": PROPERTY_HINT_FILE, + "hint_string": "*.esc" + } + ) + register_setting( ESCProjectSettingsManager.FORCE_QUIT, true, diff --git a/project.godot b/project.godot index a18ace9e..489bc0d6 100644 --- a/project.godot +++ b/project.godot @@ -792,6 +792,7 @@ dialog_simple/avatars_path="res://game/dialog_avatars/" dialog_simple/text_speed_per_character=0.1 dialog_simple/fast_text_speed_per_character=0.25 dialog_simple/max_time_to_disappear=1.0 +main/action_default_script="res://action_defaults.esc" [input]