First commit 🎉

This commit is contained in:
Tony Bark 2025-07-17 01:49:18 -04:00
commit 43ea213f9b
728 changed files with 37080 additions and 0 deletions

View file

@ -0,0 +1,419 @@
@tool
class_name PopochiuClickable
extends Area2D
## Handles an Area2D that reacts to mouse events.
##
## Is the base class for [PopochiuProp], [PopochiuHotspot] and [PopochiuCharacter].
## It has a property to determine when the object should render in front or back to other, another
## property that can be used to define the position to which characters will move to when moving
## to the item, and tow [CollisionPolygon2D] which are used to handle players interaction and
## handle scaling.
## Used to allow devs to define the cursor type for the clickable.
const CURSOR := preload("res://addons/popochiu/engine/cursor/cursor.gd")
## The identifier of the object used in scripts.
@export var script_name := ""
## The text shown to players when the cursor hovers the object.
@export var description := ""
## Whether the object will listen to interactions.
@export var clickable := true: set = set_clickable
## The [code]y[/code] position of the baseline relative to the center of the object.
@export var baseline := 0
## The [Vector2] position where characters will move when approaching the object.
@export var walk_to_point := Vector2.ZERO
## The [Vector2] position where characters will turn looking at the object.
@export var look_at_point := Vector2.ZERO
## The cursor to use when the mouse hovers the object.
@export var cursor: CURSOR.Type = CURSOR.Type.NONE
## Whether the object will be rendered always above other objects in the room.
@export var always_on_top := false
## Stores the vertices to assign to the [b]InteractionPolygon[/b] child during runtime. This is used
## by [PopochiuRoom] to store the info in its [code].tscn[/code].
@export var interaction_polygon := PackedVector2Array()
## Stores the position to assign to the [b]InteractionPolygon[/b] child during runtime. This is used
## by [PopochiuRoom] to store the info in its [code].tscn[/code].
@export var interaction_polygon_position := Vector2.ZERO
## The [PopochiuRoom] to which the object belongs.
var room: Node2D = null: set = set_room
## The number of times this object has been left-clicked.
var times_clicked := 0
## The number of times this object has been double-clicked.
var times_double_clicked := 0
## The number of times this object has been right-clicked.
var times_right_clicked := 0
## The number of times this object has been middle-clicked.
var times_middle_clicked := 0
# NOTE: Don't know if this will make sense, or if this object should emit a signal about the click
# (command execution).
## Stores the last [enum MouseButton] pressed on this object.
var last_click_button := -1
# Used for setting the double click delay. Windows default is 500 milliseconds.
var _double_click_delay: float = 0.2
# Used for tracking if a double click has occurred.
var _has_double_click: bool = false
@onready var _description_code := description
#region Godot ######################################################################################
func _ready():
add_to_group("PopochiuClickable")
if Engine.is_editor_hint():
hide_helpers()
# Add interaction polygon to the proper group
if (get_node_or_null("InteractionPolygon") != null):
get_node("InteractionPolygon").add_to_group(
PopochiuEditorHelper.POPOCHIU_OBJECT_POLYGON_GROUP
)
# Ignore assigning the polygon when:
if (
get_node_or_null("InteractionPolygon") == null # there is no InteractionPolygon node
or not get_parent() is Node2D # editing it in the .tscn file of the object directly
or self is PopochiuCharacter # avoid resetting the polygon for characters (see issue #158))
):
return
if interaction_polygon.is_empty():
interaction_polygon = get_node("InteractionPolygon").polygon
interaction_polygon_position = get_node("InteractionPolygon").position
else:
get_node("InteractionPolygon").polygon = interaction_polygon
get_node("InteractionPolygon").position = interaction_polygon_position
# If we are in the editor, we're done
return
# When the game is running...
# Update the node's polygon when:
if (
get_node_or_null("InteractionPolygon") # there is an InteractionPolygon node
and not self is PopochiuCharacter # avoids resetting the polygon (see issue #158)
):
get_node("InteractionPolygon").polygon = interaction_polygon
get_node("InteractionPolygon").position = interaction_polygon_position
visibility_changed.connect(_toggle_input)
# Ignore this object if it is a temporary one (its name has *)
if clickable and not "*" in name:
# Connect to own signals
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
# Fix #183 by listening only to inputs in this CollisionObject2D
input_event.connect(_on_input_event)
# Connect to singleton signals
PopochiuUtils.e.language_changed.connect(_translate)
_translate()
func _notification(event: int) -> void:
if event == NOTIFICATION_EDITOR_PRE_SAVE:
interaction_polygon = get_node("InteractionPolygon").polygon
interaction_polygon_position = get_node("InteractionPolygon").position
#endregion
#region Virtual ####################################################################################
## Called when the room this node belongs to has been added to the tree.
## [i]Virtual[/i].
func _on_room_set() -> void:
pass
## Called when the node is clicked.
## [i]Virtual[/i].
func _on_click() -> void:
pass
## Called when the node is double clicked.
## [i]Virtual[/i].
func _on_double_click() -> void:
pass
## Called when the node is right clicked.
## [i]Virtual[/i].
func _on_right_click() -> void:
pass
## Called when the node is middle clicked.
## [i]Virtual[/i].
func _on_middle_click() -> void:
pass
## Called when the node is clicked and there is an inventory item selected.
## [i]Virtual[/i].
func _on_item_used(item: PopochiuInventoryItem) -> void:
pass
#endregion
#region Public #####################################################################################
## Used by the plugin to hide the visual helpers that show the interaction polygon
## in the 2D Canvas Editor when this node is unselected in the Scene panel.
func hide_helpers() -> void:
if get_node_or_null("InteractionPolygon"):
$InteractionPolygon.hide()
## Used by the plugin to make visible the visual helpers that show the interaction polygon
## in the 2D Canvas Editor when this node is unselected in the Scene panel.
func show_helpers() -> void:
if get_node_or_null("InteractionPolygon"):
$InteractionPolygon.show()
## Hides this Node.[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_disable() -> Callable:
return func(): await disable()
## Hides this Node.
func disable() -> void:
self.visible = false
await get_tree().process_frame
## Shows this Node.[br][br]
## [i]This method is intended to be used inside a [method Popochiu.queue] of instructions.[/i]
func queue_enable() -> Callable:
return func(): await enable()
## Shows this Node.
func enable() -> void:
self.visible = true
await get_tree().process_frame
## Returns the [member description] of the node using [method Object.tr] if
## [member PopochiuSettings.use_translations] is [code]true[/code]. Otherwise, it returns just the
## value of [member description].
func get_description() -> String:
if Engine.is_editor_hint():
if description.is_empty():
description = name
return description
return PopochiuUtils.e.get_text(description)
## Called when the object is left clicked.
func on_click() -> void:
await _on_click()
## Called when the object is double clicked.
func on_double_click() -> void:
_reset_double_click()
await _on_double_click()
## Called when the object is right clicked.
func on_right_click() -> void:
await _on_right_click()
## Called when the object is middle clicked.
func on_middle_click() -> void:
await _on_middle_click()
## Called when an [param item] is used on this object.
func on_item_used(item: PopochiuInventoryItem) -> void:
await _on_item_used(item)
# after item has been used return to normal state
PopochiuUtils.i.active = null
## Triggers the proper GUI command for the clicked mouse button identified with [param button_idx],
## which can be [enum MouseButton].MOUSE_BUTTON_LEFT, [enum MouseButton].MOUSE_BUTTON_RIGHT or
## [enum MouseButton].MOUSE_BUTTON_MIDDLE.
func handle_command(button_idx: int) -> void:
var command: String = PopochiuUtils.e.get_current_command_name().to_snake_case()
var prefix := "on_%s"
var suffix := "click"
match button_idx:
MOUSE_BUTTON_RIGHT:
suffix = "right_" + suffix
MOUSE_BUTTON_MIDDLE:
suffix = "middle_" + suffix
if not command.is_empty():
var command_method := suffix.replace("click", command)
if has_method(prefix % command_method):
suffix = command_method
PopochiuUtils.e.add_history({
action = suffix if command.is_empty() else command,
target = description
})
await call(prefix % suffix)
#endregion
#region SetGet #####################################################################################
func set_clickable(value: bool) -> void:
clickable = value
input_pickable = clickable
func set_room(value: Node2D) -> void:
room = value
_on_room_set()
#endregion
#region Private ####################################################################################
func _on_mouse_entered() -> void:
if PopochiuUtils.e.hovered and is_instance_valid(PopochiuUtils.e.hovered) and (
PopochiuUtils.e.hovered.get_parent() == self
or get_index() < PopochiuUtils.e.hovered.get_index()
):
PopochiuUtils.e.add_hovered(self, true)
return
PopochiuUtils.e.add_hovered(self)
PopochiuUtils.g.mouse_entered_clickable.emit(self)
func _on_mouse_exited() -> void:
last_click_button = -1
if PopochiuUtils.e.remove_hovered(self):
PopochiuUtils.g.mouse_exited_clickable.emit(self)
func _on_input_event(_viewport: Node, event: InputEvent, _shape_idx: int):
if PopochiuUtils.g.is_blocked or not PopochiuUtils.e.hovered or PopochiuUtils.e.hovered != self:
return
if _is_double_click_or_tap(event):
times_double_clicked += 1
PopochiuUtils.e.clicked = self
on_double_click()
return
if not await _is_click_or_touch_pressed(event): return
var event_index := PopochiuUtils.get_click_or_touch_index(event)
PopochiuUtils.e.clicked = self
last_click_button = event_index
get_viewport().set_input_as_handled()
match event_index:
MOUSE_BUTTON_LEFT:
if PopochiuUtils.i.active:
await on_item_used(PopochiuUtils.i.active)
else:
await handle_command(event_index)
times_clicked += 1
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE:
if PopochiuUtils.i.active: return
await handle_command(event_index)
if event_index == MOUSE_BUTTON_RIGHT:
times_right_clicked += 1
elif event_index == MOUSE_BUTTON_MIDDLE:
times_middle_clicked += 1
PopochiuUtils.e.clicked = null
func _toggle_input() -> void:
if clickable:
input_pickable = visible
func _translate() -> void:
if (
Engine.is_editor_hint()
or not is_inside_tree()
or not PopochiuUtils.e.settings.use_translations
):
return
description = PopochiuUtils.e.get_text("%s-%s" % [get_tree().current_scene.name, _description_code])
# ---- @anthonyirwin82 -----------------------------------------------------------------------------
# NOTE: Temporarily duplicating PopochiuUtils functions here with an added delay for double click.
# Having delay in the PopochiuUtils class that other gui code calls introduced unwanted issues.
# This is a temporary work around until a more permanent solution is found.
# Checks if [param event] is an [InputEventMouseButton] or [InputEventScreenTouch] event.
func _is_click_or_touch(event: InputEvent) -> bool:
if (
(event is InputEventMouseButton and not event.double_click)
or (event is InputEventScreenTouch and not event.double_tap)
):
# This delay is need to prevent a single click being detected before double click
await PopochiuUtils.e.wait(_double_click_delay)
if not _has_double_click:
return (event is InputEventMouseButton or event is InputEventScreenTouch)
return false
# Checks if [param event] is an [InputEventMouseButton] or [InputEventScreenTouch] event and if it
# is pressed.
func _is_click_or_touch_pressed(event: InputEvent) -> bool:
# Fix #183 by including `event is InputEventScreenTouch` validation
if not _has_double_click:
return await _is_click_or_touch(event) and event.pressed
else:
return false
# Checks if [param event] is a double click or double tap event.
func _is_double_click_or_tap(event: InputEvent) -> bool:
if (
(event is InputEventMouseButton and event.double_click)
or (event is InputEventScreenTouch and event.double_tap)
):
_has_double_click = true
if event is InputEventMouseButton:
return event.double_click
elif event is InputEventScreenTouch:
return event.double_tap
return false
# Resets the double click status to false by default
func _reset_double_click(double_click: bool = false) -> void:
# this delay is needed to prevent single click being detected after double click event
await PopochiuUtils.e.wait(_double_click_delay)
_has_double_click = double_click
# ----------------------------------------------------------------------------- @anthonyirwin82 ----
#endregion