## 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