summaryrefslogtreecommitdiff
path: root/addons/escoria-core/game/esc_inputs_manager.gd
diff options
context:
space:
mode:
Diffstat (limited to 'addons/escoria-core/game/esc_inputs_manager.gd')
-rw-r--r--addons/escoria-core/game/esc_inputs_manager.gd886
1 files changed, 886 insertions, 0 deletions
diff --git a/addons/escoria-core/game/esc_inputs_manager.gd b/addons/escoria-core/game/esc_inputs_manager.gd
new file mode 100644
index 0000000..762da5e
--- /dev/null
+++ b/addons/escoria-core/game/esc_inputs_manager.gd
@@ -0,0 +1,886 @@
+## Escoria inputs manager
+##
+## Catches, handles and distributes input events for the game.
+## @MANAGER
+extends Resource
+class_name ESCInputsManager
+
+
+## Valid input flags[br]
+## * INPUT_ALL: All input is allowed[br]
+## * INPUT_NONE: No input is allowed at all[br]
+## * INPUT_SKIP: Only skipping dialogs is allowed
+enum {
+ INPUT_ALL,
+ INPUT_NONE,
+ INPUT_SKIP,
+}
+
+
+## Input action for use by InputMap
+const ESC_SHOW_DEBUG_PROMPT = "esc_show_debug_prompt"
+
+## Input action for use by InputMap that represents a "primary action" from an
+## input device, such as a left-click on a mouse or the X button on an XBox
+## controller
+const ESC_UI_PRIMARY_ACTION = "esc_ui_primary_action"
+
+
+## The current input mode
+var input_mode = INPUT_ALL
+
+## A LIFO stack of hovered items
+var hover_stack: HoverStack
+
+## The global id of the topmost item from the hover_stack
+var hotspot_focused: String = ""
+
+## Function reference that can be used to intercept and process input events.
+## If set, this function must have the following signature:[br]
+##[br]
+## (event: InputEvent, is_default_state: bool) -> bool[br]
+##[br]
+## #### Parameters[br]
+##[br]
+## - event: The event to process[br]
+## - is_default_state: Whether the current state is escoria.GAME_STATE.DEFAULT[br]
+##[br]
+## **Returns** Whether the function processed the event.
+var custom_input_handler = null
+
+## The currently hovered element. Usually the one on top of the hover stack.
+var _hovered_element = null
+
+## Constructor.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _init():
+ escoria.event_manager.connect("event_finished", Callable(self, "_on_event_finished"))
+ hover_stack = HoverStack.new()
+ hover_stack.connect("hover_stack_changed", Callable(self, "_on_hover_stack_changed"))
+
+## Called when an event is finished, so that the current hotspot is reset.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |return_code|`int`|The return code of the event.|yes|[br]
+## |event_name|`String`|The name of the event.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_event_finished(return_code: int, event_name: String):
+ if _hovered_element == null:
+ hotspot_focused = ""
+
+
+## Register core signals (from escoria.gd)[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func register_core():
+ escoria.game_scene.request_pause_menu.connect(_on_pause_menu_requested)
+
+## Connect the item signals to the local methods.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`Node`|The inventory item node to register.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func register_inventory_item(item: Node):
+ item.mouse_left_inventory_item.connect(_on_mouse_left_click_inventory_item)
+ item.mouse_double_left_inventory_item.connect(_on_mouse_double_left_click_inventory_item)
+ item.mouse_right_inventory_item.connect(_on_mouse_right_click_inventory_item)
+ item.inventory_item_focused.connect(_on_mouse_entered_inventory_item)
+ item.inventory_item_unfocused.connect(_on_mouse_exited_inventory_item)
+
+## Connect background signals to local methods.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |background|`ESCBackground`|The background to register.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func register_background(background: ESCBackground):
+ background.left_click_on_bg.connect(_on_left_click_on_bg)
+ background.right_click_on_bg.connect(_on_right_click_on_bg)
+ background.double_left_click_on_bg.connect(_on_double_left_click_on_bg)
+ background.mouse_wheel_up.connect(_on_mousewheel_action.bind(1))
+ background.mouse_wheel_down.connect(_on_mousewheel_action.bind(-1))
+ background.hovered_bg.connect(_on_hover_bg)
+
+
+## Registers a function that can be used to intercept and process input events. `callback` must have the following signature: (event: InputEvent, is_default_state: bool) -> bool ...where:[br]
+## [br]
+## #### Parameters[br]
+## [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]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func register_custom_input_handler(callback) -> void:
+ custom_input_handler = callback
+
+
+## If a callback was specified via `register_custom_input_handler()`, forwards the event to the callback and returns its result; otherwise,[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | 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]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns `false`. (`bool`)
+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
+
+## Callback called by hover stack content change.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_hover_stack_changed():
+ if hover_stack.is_empty():
+ unset_hovered_node(_hovered_element)
+ else:
+ set_hovered_node(hover_stack.get_top_item())
+
+
+## Sets the hovered node and calls its mouse_entered() method if it was the top most item in hover_stack.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|the item that was focused (mouse_entered)|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns a `bool` value. (`bool`)
+func set_hovered_node(item: ESCItem) -> bool:
+ if _hovered_element != item \
+ and escoria.action_manager.is_object_actionable(item.global_id) \
+ or (item is ESCPlayer and not (item as ESCPlayer).selectable):
+ _hovered_element = item
+ _hovered_element.mouse_entered()
+ return true
+ # If tested item was already hovered
+ # or is not actionable (not selectable for ESCPlayer) then do nothing
+ if _hovered_element == item \
+ or not escoria.action_manager.is_object_actionable(item.global_id) \
+ or (item is ESCPlayer and not (item as ESCPlayer).selectable):
+ return true
+ if not is_instance_valid(_hovered_element) or hover_stack.get_top_item() != item:
+ _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
+
+
+## Unsets the hovered node.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|the item that was unfocused (mouse_exited)|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func unset_hovered_node(item: ESCItem):
+ if item == null:
+ return
+ if _hovered_element == item:
+ _hovered_element.do_mouse_exited()
+ _hovered_element = null
+ hotspot_focused = ""
+
+
+# Background was hovered
+func _on_hover_bg() -> void:
+ escoria.main.current_scene.game.hovered_bg()
+
+## The background was clicked with the LMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |position|`Vector2`|Position of the click.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_left_click_on_bg(position: Vector2) -> void:
+ if input_mode == INPUT_ALL:
+ hotspot_focused = ""
+ escoria.logger.info(
+ self,
+ "Left click on background at %s." % str(position)
+ )
+ escoria.main.current_scene.game.left_click_on_bg(position)
+
+## The background was double-clicked with the LMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |position|`Vector2`|Position of the click.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_double_left_click_on_bg(position: Vector2) -> void:
+ if input_mode == INPUT_ALL:
+ hotspot_focused = ""
+ escoria.logger.info(
+ self,
+ "Double left click on background at %s." % str(position)
+ )
+ escoria.main.current_scene.game.left_double_click_on_bg(position)
+
+## The background was clicked with the RMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |position|`Vector2`|Position of the click.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_right_click_on_bg(position: Vector2) -> void:
+ if input_mode == INPUT_ALL and hotspot_focused.is_empty():
+ escoria.logger.info(
+ self,
+ "Right click on background at %s." % str(position)
+ )
+ escoria.main.current_scene.game.right_click_on_bg(position)
+
+## An inventory item was clicked with the LMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |inventory_item_global_id|`String`|The global id of the clicked inventory item.|yes|[br]
+## |event|`InputEvent`|The input event received.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_left_click_inventory_item(
+ inventory_item_global_id: String,
+ event: InputEvent
+) -> void:
+ escoria.logger.info(
+ self,
+ "Inventory item %s left clicked." % inventory_item_global_id
+ )
+ escoria.main.current_scene.game.left_click_on_inventory_item(
+ inventory_item_global_id,
+ event
+ )
+
+## An inventory item was clicked with the RMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |inventory_item_global_id|`String`|The global id of the clicked inventory item.|yes|[br]
+## |event|`InputEvent`|The input event received.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_right_click_inventory_item(
+ inventory_item_global_id: String,
+ event: InputEvent
+) -> void:
+ if input_mode == INPUT_ALL:
+ escoria.logger.info(
+ self,
+ "Inventory item %s right clicked." % inventory_item_global_id
+ )
+ escoria.main.current_scene.game.right_click_on_inventory_item(
+ inventory_item_global_id,
+ event
+ )
+
+## An inventory item was double-clicked with the LMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |inventory_item_global_id|`String`|The global id of the clicked inventory item.|yes|[br]
+## |event|`InputEvent`|The input event received.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_double_left_click_inventory_item(
+ inventory_item_global_id: String,
+ event: InputEvent
+) -> void:
+ if input_mode == INPUT_ALL:
+ escoria.logger.info(
+ self,
+ "Inventory item %s double left clicked." % inventory_item_global_id
+ )
+ escoria.main.current_scene.game.left_double_click_on_inventory_item(
+ inventory_item_global_id,
+ event
+ )
+
+## The mouse entered an inventory item.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |inventory_item_global_id|`String`|The global id of the inventory item that is hovered.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_entered_inventory_item(inventory_item_global_id: String) -> void:
+ escoria.logger.info(
+ self,
+ "Inventory item %s focused." % inventory_item_global_id
+ )
+ escoria.main.current_scene.game.inventory_item_focused(
+ inventory_item_global_id
+ )
+
+## The mouse exited an inventory item.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_exited_inventory_item() -> void:
+ escoria.logger.info(
+ self,
+ "Inventory item unfocused."
+ )
+ escoria.main.current_scene.game.inventory_item_unfocused()
+
+## The mouse entered an Escoria item.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|The Escoria item hovered.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_entered_item(item: ESCItem) -> void:
+ if item as ESCPlayer and not (item as ESCPlayer).selectable:
+ escoria.logger.trace(
+ self,
+ "Ignoring mouse entering player %s: Player not selectable." % [item.global_id]
+ )
+ if hover_stack.is_empty():
+ hotspot_focused = ""
+ escoria.main.current_scene.game.element_unfocused()
+ else:
+ hotspot_focused = hover_stack.get_top_item().global_id
+ escoria.main.current_scene.game.element_focused(hotspot_focused)
+ return
+
+ if not escoria.action_manager.is_object_actionable(item.global_id):
+ escoria.logger.debug(
+ self,
+ "Ignoring mouse entering item %s." % [item.global_id]
+ )
+ return
+
+ escoria.logger.info(
+ self,
+ "Item focused: %s" % item.global_id
+ )
+
+ hotspot_focused = item.global_id
+ escoria.main.current_scene.game.element_focused(item.global_id)
+
+## The mouse exited an Escoria item.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|The Escoria item hovered.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_exited_item(item: ESCItem) -> void:
+ var object: ESCObject = escoria.object_manager.get_object(item.global_id)
+ if object and not object.interactive:
+ hover_stack.erase_item(item)
+ escoria.main.current_scene.game.element_unfocused()
+ return
+
+ if object and not object.interactive:
+ return
+ if object and is_instance_valid(object.node) and object.node is ESCPlayer and not (object.node as ESCPlayer).selectable:
+ hotspot_focused = ""
+ return
+
+ escoria.logger.info(
+ self,
+ "Item unfocused: %s" % hotspot_focused
+ )
+
+ if hover_stack.is_empty():
+ hotspot_focused = ""
+ escoria.main.current_scene.game.element_unfocused()
+ else:
+ hotspot_focused = hover_stack.get_top_item().global_id
+ escoria.main.current_scene.game.element_focused(hotspot_focused)
+
+
+## Function called when the item is set to non-interactive, to re-trigger an input on underlying item.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|The `ESCItem` that was set non-interactive|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func on_item_non_interactive(item: ESCItem) -> void:
+ var object: ESCObject = escoria.object_manager.get_object(item.global_id)
+ if object and not object.interactive:
+ hover_stack.erase_item(item)
+ escoria.main.current_scene.game.element_unfocused()
+
+ 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()
+
+
+# An Escoria item was clicked with the LMB
+#
+# #### Parameters
+#
+# - item: The Escoria item clicked
+# - event: The input event from the click
+func _on_mouse_left_clicked_item(item: ESCItem, event: InputEvent) -> void:
+ if input_mode == INPUT_ALL:
+ # Manage clicking through ESCPlayer (if ESCPlayer.selectable is false)
+ if item as ESCPlayer and not (item as ESCPlayer).selectable:
+ escoria.logger.trace(
+ self,
+ "Ignoring left click on player %s: Player not selectable."
+ % [item.global_id]
+ )
+
+ # Get next object in hover stack and forward event to it
+ if not hover_stack.is_empty():
+ var next_item = hover_stack.pop_top_item()
+ _on_mouse_left_clicked_item(next_item, event)
+ else: # if no next object, consider this click as background click
+ hotspot_focused = ""
+ _on_left_click_on_bg(event.position)
+ return
+
+ # Clicked object can't be actioned and there is no other object behind
+ # We consider this click as a background click
+ if not escoria.action_manager.is_object_actionable(item.global_id) \
+ and hover_stack.is_empty():
+ hotspot_focused = ""
+ _on_left_click_on_bg(event.position)
+ return
+
+ # Finally, execute the action on the ESCItem
+ hotspot_focused = item.global_id
+ escoria.main.current_scene.game.left_click_on_item(
+ item.global_id,
+ event
+ )
+
+## An Escoria item was double-clicked with the LMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|The Escoria item clicked.|yes|[br]
+## |event|`InputEvent`|The input event from the click.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_left_double_clicked_item(
+ item: ESCItem,
+ event: InputEvent
+) -> void:
+ if input_mode == INPUT_ALL:
+ # Manage clicking through ESCPlayer (if ESCPlayer.selectable is false)
+ if item as ESCPlayer and not (item as ESCPlayer).selectable:
+ escoria.logger.trace(
+ self,
+ "Ignoring double left click on player %s: Player not selectable."
+ % [item.global_id]
+ )
+
+ # Get next object in hover stack and forward event to it
+ if not hover_stack.is_empty():
+ var next_item = hover_stack.pop_top_item()
+ _on_mouse_left_double_clicked_item(next_item, event)
+ else: # if no next object, consider this click as background click
+ hotspot_focused = ""
+ _on_double_left_click_on_bg(event.position)
+ return
+
+ # Clicked object can't be actioned and there is no other object behind
+ # We consider this click as a background click
+ if not escoria.action_manager.is_object_actionable(item.global_id) \
+ and hover_stack.is_empty():
+ hotspot_focused = ""
+ _on_double_left_click_on_bg(event.position)
+ return
+
+ # Finally, execute the action on the ESCItem
+ hotspot_focused = item.global_id
+ escoria.main.current_scene.game.left_double_click_on_item(
+ item.global_id,
+ event
+ )
+
+## An Escoria item was clicked with the RMB.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## | Name | Type | Description | Required? |[br]
+## |:-----|:-----|:------------|:----------|[br]
+## |item|`ESCItem`|The Escoria item clicked.|yes|[br]
+## |event|`InputEvent`|The input event from the click.|yes|[br]
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_mouse_right_clicked_item(item: ESCItem, event: InputEvent) -> void:
+ if input_mode == INPUT_ALL:
+ if item as ESCPlayer and not (item as ESCPlayer).selectable:
+ escoria.logger.debug(
+ self,
+ "Ignoring right click on player %s: Player not selectable." % [item.global_id]
+ )
+
+ if not hover_stack.is_empty():
+ var next_item = hover_stack.pop_top_item()
+ _on_mouse_right_clicked_item(next_item, event)
+ return
+
+ if not escoria.action_manager.is_object_actionable(item.global_id) \
+ and hover_stack.is_empty():
+ # Treat this as a background click now
+ hotspot_focused = ""
+ _on_right_click_on_bg(event.position)
+ return
+
+ var actual_item
+
+ # We check if the clicked object is ESCPlayer and not selectable. If so
+ # we consider we clicked through it.
+ var object: ESCObject = escoria.object_manager.get_object(item.global_id)
+ if object.node is ESCPlayer and not (object.node as ESCPlayer).selectable:
+ actual_item = hover_stack.get_top_item()
+ else:
+ actual_item = item
+
+ if actual_item == null:
+ if event.position:
+ (escoria.main.current_scene.game as ESCGame).right_click_on_bg(event.position)
+ else:
+ escoria.logger.info(
+ self,
+ "Clicked item %s with event %s cannot be activated (player not selectable or not interactive).\n"
+ % [item.global_id, event] +
+ "No valid item found in the items stack. Action canceled."
+ )
+ else:
+ escoria.logger.info(
+ self,
+ "Item %s right clicked with event %s." % [actual_item.global_id, event]
+ )
+ hotspot_focused = actual_item.global_id
+ escoria.main.current_scene.game.right_click_on_item(
+ actual_item.global_id,
+ event
+ )
+
+
+# The mousewheel was turned
+#
+# #### Parameters
+#
+# - direction: The direction the wheel was turned. 1 = up, -1 = down
+func _on_mousewheel_action(direction: int):
+ escoria.main.current_scene.game.mousewheel_action(direction)
+
+## Event when the pause menu was requested.[br]
+## [br]
+## #### Parameters[br]
+## [br]
+## None.
+## [br]
+## #### Returns[br]
+## [br]
+## Returns nothing.
+func _on_pause_menu_requested():
+ escoria.main.current_scene.game.pause_game()
+
+
+## Hover Stack implementation.
+class HoverStack:
+
+
+ ## Emitted when the content of the hover stack has changed[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ signal hover_stack_changed
+
+ ## Emitted when the hover stack was emptied[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ signal hover_stack_emptied
+
+
+ ## Array representing the hover stack
+ var hover_stack: Array = []
+
+
+ ## Add the given item to the stack if not already in it.[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## | Name | Type | Description | Required? |[br]
+ ## |:-----|:-----|:------------|:----------|[br]
+ ## |item|`Variant`|the item to add to the hover stack|yes|[br]
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func add_item(item):
+ if item is ESCPlayer and not (item as ESCPlayer).selectable:
+ return
+ if not hover_stack.has(item):
+ hover_stack.push_back(item)
+ _sort()
+ hover_stack_changed.emit()
+
+
+ ## Add the items contained in given list to the stack if not already in it.[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## | Name | Type | Description | Required? |[br]
+ ## |:-----|:-----|:------------|:----------|[br]
+ ## |items|`Array`|the items list (array) to add to the hover stack|yes|[br]
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func add_items(items: Array):
+ for item in items:
+ if escoria.action_manager.is_object_actionable(item.global_id):
+ add_item(item)
+
+
+ ## Clean the hover stack[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func clean():
+ for e in hover_stack:
+ if e == null or !is_instance_valid(e):
+ hover_stack.erase(e)
+ hover_stack_changed.emit()
+
+
+ ## Pops the top element of the hover stack and returns it[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func pop_top_item():
+ var ret = hover_stack.pop_back()
+ if is_instance_valid(ret):
+ hover_stack_changed.emit()
+ return ret
+
+
+ ## The top element of the hover stack[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns the top element of the hover stack. (`Variant`)
+ func get_top_item():
+ return hover_stack.back()
+
+
+ ## Remove the given item from the stack[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## | Name | Type | Description | Required? |[br]
+ ## |:-----|:-----|:------------|:----------|[br]
+ ## |item|`Variant`|the item to remove from the hover stack|yes|[br]
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func erase_item(item):
+ if hover_stack.has(item):
+ hover_stack.erase(item)
+ _sort()
+ hover_stack_changed.emit()
+
+
+ ## Clear the stack of hovered items[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func clear():
+ hover_stack = []
+ hover_stack_emptied.emit()
+
+
+ ## True if the hover stack is empty, else false[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns true if the hover stack is empty, else false. (`bool`)
+ func is_empty() -> bool:
+ return hover_stack.is_empty()
+
+
+ ## Sort the hover stack by items' z-index.[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns nothing.
+ func _sort():
+ hover_stack.sort_custom(Callable(HoverStackSorter, "sort_ascending_z_index"))
+
+
+ ## True if the hover stack contains the given item[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## | Name | Type | Description | Required? |[br]
+ ## |:-----|:-----|:------------|:----------|[br]
+ ## |item|`Variant`|the item to search|yes|[br]
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns true if the hover stack contains the given item. (`bool`)
+ func has(item) -> bool:
+ return hover_stack.has(item)
+
+
+ ## The hover stack array[br]
+ ## [br]
+ ## #### Parameters[br]
+ ## [br]
+ ## None.
+ ## [br]
+ ## #### Returns[br]
+ ## [br]
+ ## Returns the hover stack array. (`Array`)
+ func get_all() -> Array:
+ return hover_stack
+
+ ## Z Sorter class for hover stack
+ class HoverStackSorter:
+ static func sort_ascending_z_index(a, b):
+ if a.z_index < b.z_index:
+ return true
+ return false