refactor: unify interactable components
This commit is contained in:
96
_Core/interactions/InteractableComponent.gd
Normal file
96
_Core/interactions/InteractableComponent.gd
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
class_name InteractableComponent
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# 可挂载于任意 Node2D 宿主的交互组件。
|
||||||
|
# 它统一负责交互锚点、距离过滤、白圈位置和静态动作;复杂宿主可通过
|
||||||
|
# build_interaction_actions(component, player) 返回多个 InteractionAction。
|
||||||
|
const GROUP: StringName = &"whaletown_interactable_component"
|
||||||
|
|
||||||
|
@export_category("Interaction")
|
||||||
|
@export var interaction_id: String = ""
|
||||||
|
@export var interaction_title: String = ""
|
||||||
|
@export var interaction_priority: int = 100
|
||||||
|
@export var interaction_distance: float = 150.0
|
||||||
|
@export var activation_method: StringName = &""
|
||||||
|
@export var show_marker: bool = true
|
||||||
|
|
||||||
|
@export_category("Anchor")
|
||||||
|
@export var anchor_path: NodePath = NodePath("")
|
||||||
|
|
||||||
|
var _host: Node2D
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_host = get_parent() as Node2D
|
||||||
|
if _host == null:
|
||||||
|
push_error("InteractableComponent 必须挂在 Node2D 宿主下:%s" % get_path())
|
||||||
|
return
|
||||||
|
add_to_group(GROUP)
|
||||||
|
|
||||||
|
func get_actions(player: Node2D) -> Array[InteractionAction]:
|
||||||
|
var actions: Array[InteractionAction] = []
|
||||||
|
if player == null or not is_interaction_active():
|
||||||
|
return actions
|
||||||
|
var distance: float = get_anchor_position().distance_to(player.global_position)
|
||||||
|
if distance > interaction_distance:
|
||||||
|
return actions
|
||||||
|
if _host != null and _host.has_method("build_interaction_actions"):
|
||||||
|
var actions_variant: Variant = _host.call("build_interaction_actions", self, player)
|
||||||
|
if actions_variant is Array:
|
||||||
|
for action_variant: Variant in actions_variant as Array:
|
||||||
|
if action_variant is InteractionAction:
|
||||||
|
var action: InteractionAction = action_variant as InteractionAction
|
||||||
|
if action.is_valid():
|
||||||
|
action.distance = distance
|
||||||
|
actions.append(action)
|
||||||
|
return actions
|
||||||
|
var default_action: InteractionAction = _create_default_action()
|
||||||
|
if default_action != null:
|
||||||
|
default_action.distance = distance
|
||||||
|
actions.append(default_action)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
func get_marker_positions() -> Array[Vector2]:
|
||||||
|
if not show_marker or not is_interaction_active():
|
||||||
|
return []
|
||||||
|
return [get_anchor_position()]
|
||||||
|
|
||||||
|
func get_anchor_position() -> Vector2:
|
||||||
|
var anchor: Node2D = _resolve_anchor()
|
||||||
|
if anchor == null:
|
||||||
|
return Vector2.ZERO
|
||||||
|
var collision_shape: CollisionShape2D = _first_enabled_collision_shape(anchor)
|
||||||
|
return collision_shape.global_position if collision_shape != null else anchor.global_position
|
||||||
|
|
||||||
|
func is_interaction_active() -> bool:
|
||||||
|
if _host == null:
|
||||||
|
return false
|
||||||
|
if _host.has_method("is_interaction_active"):
|
||||||
|
return bool(_host.call("is_interaction_active", self))
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _create_default_action() -> InteractionAction:
|
||||||
|
if _host == null or activation_method.is_empty() or interaction_id.strip_edges().is_empty() or interaction_title.strip_edges().is_empty():
|
||||||
|
return null
|
||||||
|
if not _host.has_method(activation_method):
|
||||||
|
push_warning("InteractableComponent: %s 不存在方法 %s" % [_host.get_path(), activation_method])
|
||||||
|
return null
|
||||||
|
return InteractionAction.create(interaction_id, interaction_title, interaction_priority, Callable(_host, activation_method))
|
||||||
|
|
||||||
|
func _resolve_anchor() -> Node2D:
|
||||||
|
if _host == null:
|
||||||
|
return null
|
||||||
|
if not anchor_path.is_empty():
|
||||||
|
var explicit_anchor: Node2D = _host.get_node_or_null(anchor_path) as Node2D
|
||||||
|
if explicit_anchor != null:
|
||||||
|
return explicit_anchor
|
||||||
|
return _host
|
||||||
|
|
||||||
|
func _first_enabled_collision_shape(anchor: Node2D) -> CollisionShape2D:
|
||||||
|
var direct_shape: CollisionShape2D = anchor as CollisionShape2D
|
||||||
|
if direct_shape != null and not direct_shape.disabled and direct_shape.shape != null:
|
||||||
|
return direct_shape
|
||||||
|
for child: Node in anchor.get_children():
|
||||||
|
var collision_shape: CollisionShape2D = child as CollisionShape2D
|
||||||
|
if collision_shape != null and not collision_shape.disabled and collision_shape.shape != null:
|
||||||
|
return collision_shape
|
||||||
|
return null
|
||||||
1
_Core/interactions/InteractableComponent.gd.uid
Normal file
1
_Core/interactions/InteractableComponent.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cn3fccr0qc41h
|
||||||
20
_Core/interactions/InteractionAction.gd
Normal file
20
_Core/interactions/InteractionAction.gd
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class_name InteractionAction
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
# 交互管理器使用的强类型动作数据,避免各交互物品以 Dictionary 约定字段。
|
||||||
|
var id: String = ""
|
||||||
|
var title: String = ""
|
||||||
|
var priority: int = 100
|
||||||
|
var distance: float = INF
|
||||||
|
var activate: Callable = Callable()
|
||||||
|
|
||||||
|
static func create(action_id: String, action_title: String, action_priority: int, callback: Callable) -> InteractionAction:
|
||||||
|
var action: InteractionAction = InteractionAction.new()
|
||||||
|
action.id = action_id
|
||||||
|
action.title = action_title
|
||||||
|
action.priority = action_priority
|
||||||
|
action.activate = callback
|
||||||
|
return action
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return not id.strip_edges().is_empty() and not title.strip_edges().is_empty() and activate.is_valid()
|
||||||
1
_Core/interactions/InteractionAction.gd.uid
Normal file
1
_Core/interactions/InteractionAction.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dbrui8h3sbjrn
|
||||||
@@ -6,9 +6,8 @@ const MAX_ACTIONS_VISIBLE: int = 5
|
|||||||
const ACCENT: Color = Color("58c7db")
|
const ACCENT: Color = Color("58c7db")
|
||||||
const PANEL_COLOR: Color = Color(0.035, 0.071, 0.106, 0.94)
|
const PANEL_COLOR: Color = Color(0.035, 0.071, 0.106, 0.94)
|
||||||
const INTERACTION_POINT_MARKERS_SCRIPT: Script = preload("res://_Core/ui/InteractionPointMarkers.gd")
|
const INTERACTION_POINT_MARKERS_SCRIPT: Script = preload("res://_Core/ui/InteractionPointMarkers.gd")
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
var _actions: Array[Dictionary] = []
|
var _actions: Array[InteractionAction] = []
|
||||||
var _selected_index: int = 0
|
var _selected_index: int = 0
|
||||||
var _last_selected_id: String = ""
|
var _last_selected_id: String = ""
|
||||||
var _scan_elapsed: float = SCAN_INTERVAL
|
var _scan_elapsed: float = SCAN_INTERVAL
|
||||||
@@ -58,32 +57,23 @@ func _refresh_actions() -> void:
|
|||||||
if player == null:
|
if player == null:
|
||||||
_set_actions([])
|
_set_actions([])
|
||||||
return
|
return
|
||||||
var collected: Array[Dictionary] = []
|
var collected: Array[InteractionAction] = []
|
||||||
for node in get_tree().get_nodes_in_group("whaletown_interactable"):
|
for node: Node in get_tree().get_nodes_in_group(InteractableComponent.GROUP):
|
||||||
if not is_instance_valid(node) or not node.has_method("get_interaction_actions"):
|
var interactable: InteractableComponent = node as InteractableComponent
|
||||||
|
if interactable == null:
|
||||||
continue
|
continue
|
||||||
var actions_variant: Variant = node.call("get_interaction_actions", player)
|
for action: InteractionAction in interactable.get_actions(player):
|
||||||
if not (actions_variant is Array):
|
|
||||||
continue
|
|
||||||
for item in actions_variant as Array:
|
|
||||||
if item is Dictionary:
|
|
||||||
var action: Dictionary = item
|
|
||||||
if bool(action.get("available", true)):
|
|
||||||
collected.append(action)
|
collected.append(action)
|
||||||
collected.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
collected.sort_custom(func(a: InteractionAction, b: InteractionAction) -> bool:
|
||||||
var priority_a := int(a.get("priority", 100))
|
if a.priority != b.priority:
|
||||||
var priority_b := int(b.get("priority", 100))
|
return a.priority < b.priority
|
||||||
if priority_a != priority_b:
|
if not is_equal_approx(a.distance, b.distance):
|
||||||
return priority_a < priority_b
|
return a.distance < b.distance
|
||||||
var distance_a := float(a.get("distance", INF))
|
return a.title < b.title
|
||||||
var distance_b := float(b.get("distance", INF))
|
|
||||||
if not is_equal_approx(distance_a, distance_b):
|
|
||||||
return distance_a < distance_b
|
|
||||||
return str(a.get("title", "")) < str(b.get("title", ""))
|
|
||||||
)
|
)
|
||||||
_set_actions(collected)
|
_set_actions(collected)
|
||||||
|
|
||||||
func _set_actions(next_actions: Array[Dictionary]) -> void:
|
func _set_actions(next_actions: Array[InteractionAction]) -> void:
|
||||||
_actions = next_actions
|
_actions = next_actions
|
||||||
if _actions.is_empty():
|
if _actions.is_empty():
|
||||||
_selected_index = 0
|
_selected_index = 0
|
||||||
@@ -92,30 +82,29 @@ func _set_actions(next_actions: Array[Dictionary]) -> void:
|
|||||||
return
|
return
|
||||||
var matched_index := -1
|
var matched_index := -1
|
||||||
for index in _actions.size():
|
for index in _actions.size():
|
||||||
if str(_actions[index].get("id", "")) == _last_selected_id:
|
if _actions[index].id == _last_selected_id:
|
||||||
matched_index = index
|
matched_index = index
|
||||||
break
|
break
|
||||||
_selected_index = matched_index if matched_index >= 0 else clampi(_selected_index, 0, _actions.size() - 1)
|
_selected_index = matched_index if matched_index >= 0 else clampi(_selected_index, 0, _actions.size() - 1)
|
||||||
_last_selected_id = str(_actions[_selected_index].get("id", ""))
|
_last_selected_id = _actions[_selected_index].id
|
||||||
_render()
|
_render()
|
||||||
|
|
||||||
func _select_offset(offset: int) -> void:
|
func _select_offset(offset: int) -> void:
|
||||||
if _actions.is_empty():
|
if _actions.is_empty():
|
||||||
return
|
return
|
||||||
_selected_index = posmod(_selected_index + offset, _actions.size())
|
_selected_index = posmod(_selected_index + offset, _actions.size())
|
||||||
_last_selected_id = str(_actions[_selected_index].get("id", ""))
|
_last_selected_id = _actions[_selected_index].id
|
||||||
_render()
|
_render()
|
||||||
|
|
||||||
func _execute_selected() -> void:
|
func _execute_selected() -> void:
|
||||||
if _executing or _actions.is_empty():
|
if _executing or _actions.is_empty():
|
||||||
return
|
return
|
||||||
var action := _actions[_selected_index]
|
var action: InteractionAction = _actions[_selected_index]
|
||||||
var callback_variant: Variant = action.get("callback", Callable())
|
if not action.activate.is_valid():
|
||||||
if not (callback_variant is Callable) or not (callback_variant as Callable).is_valid():
|
|
||||||
return
|
return
|
||||||
_executing = true
|
_executing = true
|
||||||
_render()
|
_render()
|
||||||
(callback_variant as Callable).call()
|
action.activate.call()
|
||||||
await get_tree().create_timer(0.18).timeout
|
await get_tree().create_timer(0.18).timeout
|
||||||
_executing = false
|
_executing = false
|
||||||
_refresh_actions()
|
_refresh_actions()
|
||||||
@@ -168,13 +157,13 @@ func _render() -> void:
|
|||||||
var start: int = clampi(_selected_index - 2, 0, maxi(0, _actions.size() - MAX_ACTIONS_VISIBLE))
|
var start: int = clampi(_selected_index - 2, 0, maxi(0, _actions.size() - MAX_ACTIONS_VISIBLE))
|
||||||
var finish: int = mini(_actions.size(), start + MAX_ACTIONS_VISIBLE)
|
var finish: int = mini(_actions.size(), start + MAX_ACTIONS_VISIBLE)
|
||||||
for index in range(start, finish):
|
for index in range(start, finish):
|
||||||
var action := _actions[index]
|
var action: InteractionAction = _actions[index]
|
||||||
var row := Label.new()
|
var row := Label.new()
|
||||||
row.custom_minimum_size = Vector2(0, 29)
|
row.custom_minimum_size = Vector2(0, 29)
|
||||||
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
row.add_theme_font_size_override("font_size", 16)
|
row.add_theme_font_size_override("font_size", 16)
|
||||||
var selected := index == _selected_index
|
var selected := index == _selected_index
|
||||||
row.text = ("› " if selected else " ") + str(action.get("title", "交互")) + (" …" if selected and _executing else "")
|
row.text = ("› " if selected else " ") + action.title + (" …" if selected and _executing else "")
|
||||||
row.add_theme_color_override("font_color", ACCENT if selected else Color(0.92, 0.96, 0.98, 1.0))
|
row.add_theme_color_override("font_color", ACCENT if selected else Color(0.92, 0.96, 0.98, 1.0))
|
||||||
if selected:
|
if selected:
|
||||||
row.add_theme_stylebox_override("normal", _selected_style())
|
row.add_theme_stylebox_override("normal", _selected_style())
|
||||||
@@ -191,19 +180,12 @@ func _render_interaction_points() -> void:
|
|||||||
if markers == null:
|
if markers == null:
|
||||||
return
|
return
|
||||||
var positions: Array[Vector2] = []
|
var positions: Array[Vector2] = []
|
||||||
for node: Node in get_tree().get_nodes_in_group("whaletown_interactable"):
|
for node: Node in get_tree().get_nodes_in_group(InteractableComponent.GROUP):
|
||||||
if not is_instance_valid(node) or not node.has_method("get_interaction_actions"):
|
var interactable: InteractableComponent = node as InteractableComponent
|
||||||
|
if interactable == null:
|
||||||
continue
|
continue
|
||||||
if node.has_method("get_interaction_marker_positions"):
|
for position: Vector2 in interactable.get_marker_positions():
|
||||||
var marker_positions_variant: Variant = node.call("get_interaction_marker_positions")
|
positions.append(position)
|
||||||
if marker_positions_variant is Array:
|
|
||||||
for position_variant: Variant in marker_positions_variant as Array:
|
|
||||||
if position_variant is Vector2:
|
|
||||||
positions.append(position_variant as Vector2)
|
|
||||||
continue
|
|
||||||
var source_node: Node2D = node as Node2D
|
|
||||||
if source_node != null:
|
|
||||||
positions.append(InteractionAnchorUtil.get_position(source_node))
|
|
||||||
markers.call("set_points", positions)
|
markers.call("set_points", positions)
|
||||||
|
|
||||||
func _ensure_point_markers() -> Node2D:
|
func _ensure_point_markers() -> Node2D:
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
class_name InteractionAnchor
|
|
||||||
extends RefCounted
|
|
||||||
|
|
||||||
# 所有交互距离与地图标记共用的世界锚点。
|
|
||||||
# Area2D 优先使用实际 CollisionShape2D 的中心,避免脚本节点原点与碰撞区域有偏移时
|
|
||||||
# 出现“能交互的位置”和白圈不一致的问题。
|
|
||||||
static func get_position(node: Node2D) -> Vector2:
|
|
||||||
var area: Area2D = node as Area2D
|
|
||||||
if area != null:
|
|
||||||
for child: Node in area.get_children():
|
|
||||||
var collision_shape: CollisionShape2D = child as CollisionShape2D
|
|
||||||
if collision_shape != null and not collision_shape.disabled and collision_shape.shape != null:
|
|
||||||
return collision_shape.global_position
|
|
||||||
return node.global_position
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dogltsticcpy2
|
|
||||||
@@ -23,7 +23,6 @@ const COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH: int = 118
|
|||||||
const COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
const COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
||||||
const NPC_NAMEPLATE_OFFSET_Y: float = -136.0
|
const NPC_NAMEPLATE_OFFSET_Y: float = -136.0
|
||||||
const HIRED_PLAYER_NAMEPLATE_OFFSET_Y: float = -96.0
|
const HIRED_PLAYER_NAMEPLATE_OFFSET_Y: float = -96.0
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||||
@@ -39,7 +38,6 @@ var _isChangingScene: bool = false
|
|||||||
var _lastRecruitmentClickMsec: int = 0
|
var _lastRecruitmentClickMsec: int = 0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("whaletown_interactable")
|
|
||||||
_align_service_occupants()
|
_align_service_occupants()
|
||||||
_configure_static_companion_nameplates()
|
_configure_static_companion_nameplates()
|
||||||
_apply_spawn_point()
|
_apply_spawn_point()
|
||||||
@@ -47,6 +45,7 @@ func _ready() -> void:
|
|||||||
_connect_exit_area()
|
_connect_exit_area()
|
||||||
_connect_recruitment_area()
|
_connect_recruitment_area()
|
||||||
_connect_cafe_companion_events()
|
_connect_cafe_companion_events()
|
||||||
|
_register_interactables()
|
||||||
_discover_destination()
|
_discover_destination()
|
||||||
|
|
||||||
func _discover_destination() -> void:
|
func _discover_destination() -> void:
|
||||||
@@ -103,29 +102,28 @@ func _connect_cafe_companion_events() -> void:
|
|||||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
|
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
|
||||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
|
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
|
||||||
|
|
||||||
|
func _register_interactables() -> void:
|
||||||
|
var exit_interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
exit_interactable.interaction_id = "cafe_exit"
|
||||||
|
exit_interactable.interaction_title = "离开咖啡馆"
|
||||||
|
exit_interactable.interaction_priority = 20
|
||||||
|
exit_interactable.interaction_distance = 160.0
|
||||||
|
exit_interactable.activation_method = &"_leave_to_work_zone"
|
||||||
|
exit_interactable.anchor_path = NodePath("InteractionAreas/ExitToWorkZoneArea")
|
||||||
|
add_child(exit_interactable)
|
||||||
|
|
||||||
|
var recruitment_interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
recruitment_interactable.interaction_id = "cafe_recruitment"
|
||||||
|
recruitment_interactable.interaction_title = "登记咖啡店陪伴机器人"
|
||||||
|
recruitment_interactable.interaction_priority = 30
|
||||||
|
recruitment_interactable.interaction_distance = 150.0
|
||||||
|
recruitment_interactable.activation_method = &"_try_emit_recruitment_selected"
|
||||||
|
recruitment_interactable.anchor_path = NodePath("InteractionAreas/CafeRecruitmentLogoArea")
|
||||||
|
add_child(recruitment_interactable)
|
||||||
|
|
||||||
func _on_exit_area_body_entered(body: Node2D) -> void:
|
func _on_exit_area_body_entered(body: Node2D) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
func get_interaction_actions(local_player: Node2D) -> Array[Dictionary]:
|
|
||||||
if _isChangingScene or exitToWorkZoneArea == null:
|
|
||||||
return []
|
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(exitToWorkZoneArea)
|
|
||||||
var distance: float = anchor_position.distance_to(local_player.global_position)
|
|
||||||
if distance > 160.0:
|
|
||||||
return []
|
|
||||||
return [{
|
|
||||||
"id": "cafe_exit",
|
|
||||||
"title": "离开咖啡馆",
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 20,
|
|
||||||
"callback": Callable(self, "_leave_to_work_zone"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func get_interaction_marker_positions() -> Array[Vector2]:
|
|
||||||
if exitToWorkZoneArea == null:
|
|
||||||
return []
|
|
||||||
return [InteractionAnchorUtil.get_position(exitToWorkZoneArea)]
|
|
||||||
|
|
||||||
func _leave_to_work_zone() -> void:
|
func _leave_to_work_zone() -> void:
|
||||||
if _isChangingScene:
|
if _isChangingScene:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
class_name ScenePortal
|
class_name ScenePortal
|
||||||
extends Area2D
|
extends Area2D
|
||||||
|
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ScenePortal.gd - 场景入口传送组件
|
# ScenePortal.gd - 场景入口传送组件
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -16,27 +14,20 @@ const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|||||||
@export var interactionDistance: float = 160.0
|
@export var interactionDistance: float = 160.0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("whaletown_interactable")
|
var label: String = interactionTitle.strip_edges()
|
||||||
|
if label.is_empty():
|
||||||
|
label = "进入 %s" % targetSceneName
|
||||||
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_id = "portal:%s" % str(get_path())
|
||||||
|
interactable.interaction_title = label
|
||||||
|
interactable.interaction_priority = 20
|
||||||
|
interactable.interaction_distance = interactionDistance
|
||||||
|
interactable.activation_method = &"_change_scene"
|
||||||
|
add_child(interactable)
|
||||||
|
|
||||||
func _on_body_entered(body: Node2D) -> void:
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
|
||||||
if distance > interactionDistance:
|
|
||||||
return []
|
|
||||||
var label := interactionTitle.strip_edges()
|
|
||||||
if label.is_empty():
|
|
||||||
label = "进入 %s" % targetSceneName
|
|
||||||
return [{
|
|
||||||
"id": "portal:%s" % get_instance_id(),
|
|
||||||
"title": label,
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 20,
|
|
||||||
"callback": Callable(self, "_change_scene"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func _change_scene() -> void:
|
func _change_scene() -> void:
|
||||||
if targetSceneName.is_empty():
|
if targetSceneName.is_empty():
|
||||||
push_warning("ScenePortal: targetSceneName is empty.")
|
push_warning("ScenePortal: targetSceneName is empty.")
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ var _isInsideMall: bool = false
|
|||||||
var _mallCanEnter: bool = true
|
var _mallCanEnter: bool = true
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("whaletown_interactable")
|
|
||||||
_apply_spawn_point()
|
_apply_spawn_point()
|
||||||
_configure_camera()
|
_configure_camera()
|
||||||
_ensure_mall_panel()
|
_ensure_mall_panel()
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ extends Area2D
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
const INTERACTION_COLLISION_LAYER: int = 2
|
const INTERACTION_COLLISION_LAYER: int = 2
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
@export var buildingId: String = ""
|
@export var buildingId: String = ""
|
||||||
@export var buildingTitle: String = ""
|
@export var buildingTitle: String = ""
|
||||||
@@ -18,20 +17,18 @@ const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
collision_layer = INTERACTION_COLLISION_LAYER
|
collision_layer = INTERACTION_COLLISION_LAYER
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
add_to_group("whaletown_interactable")
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_distance = interactionDistance
|
||||||
|
add_child(interactable)
|
||||||
|
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
func is_interaction_active(_component: InteractableComponent) -> bool:
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
return not buildingId.strip_edges().is_empty()
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
|
||||||
if distance > interactionDistance:
|
func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]:
|
||||||
return []
|
var actions: Array[InteractionAction] = []
|
||||||
return [{
|
var title: String = buildingTitle if not buildingTitle.is_empty() else "设施"
|
||||||
"id": "building:%s" % buildingId,
|
actions.append(InteractionAction.create("building:%s" % buildingId, "使用 %s" % title, 30, Callable(self, "interact")))
|
||||||
"title": "使用 %s" % (buildingTitle if not buildingTitle.is_empty() else "设施"),
|
return actions
|
||||||
"distance": distance,
|
|
||||||
"priority": 30,
|
|
||||||
"callback": Callable(self, "interact"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
var payload := {
|
var payload := {
|
||||||
|
|||||||
@@ -19,9 +19,22 @@ class_name CafeCompanionTarget
|
|||||||
var _lastClickMsec: int = 0
|
var _lastClickMsec: int = 0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_distance = 160.0
|
||||||
|
add_child(interactable)
|
||||||
input_pickable = true
|
input_pickable = true
|
||||||
set_process_unhandled_input(true)
|
set_process_unhandled_input(true)
|
||||||
|
|
||||||
|
func is_interaction_active(_component: InteractableComponent) -> bool:
|
||||||
|
return not servicePointId.strip_edges().is_empty() and not companionId.strip_edges().is_empty()
|
||||||
|
|
||||||
|
func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]:
|
||||||
|
var actions: Array[InteractionAction] = []
|
||||||
|
var display_name: String = _resolved_persona_name()
|
||||||
|
var action_id: String = "cafe_companion:%s" % servicePointId
|
||||||
|
actions.append(InteractionAction.create(action_id, "与 %s 交流" % display_name, 45, Callable(self, "_try_emit_target_selected")))
|
||||||
|
return actions
|
||||||
|
|
||||||
func _input_event(_viewport: Viewport, event: InputEvent, _shapeIdx: int) -> void:
|
func _input_event(_viewport: Viewport, event: InputEvent, _shapeIdx: int) -> void:
|
||||||
if not (event is InputEventMouseButton):
|
if not (event is InputEventMouseButton):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class_name NPCController
|
|||||||
signal interaction_happened(text: String)
|
signal interaction_happened(text: String)
|
||||||
|
|
||||||
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
|
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
|
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
|
||||||
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
||||||
const NPC_TALKED_EVENT: String = "npc_talked"
|
const NPC_TALKED_EVENT: String = "npc_talked"
|
||||||
@@ -42,7 +41,14 @@ const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
|||||||
var _nameplate: Label
|
var _nameplate: Label
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("whaletown_interactable")
|
if get_node_or_null("CafeCompanionTarget") == null:
|
||||||
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_id = "npc:%s" % str(get_path())
|
||||||
|
interactable.interaction_title = "与 %s 交谈" % npcName
|
||||||
|
interactable.interaction_priority = 40
|
||||||
|
interactable.interaction_distance = interactionDistance
|
||||||
|
interactable.activation_method = &"interact"
|
||||||
|
add_child(interactable)
|
||||||
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
|
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
|
||||||
if animation_player.has_animation("idle"):
|
if animation_player.has_animation("idle"):
|
||||||
animation_player.play("idle")
|
animation_player.play("idle")
|
||||||
@@ -68,19 +74,6 @@ func interact() -> void:
|
|||||||
})
|
})
|
||||||
interaction_happened.emit(dialogue)
|
interaction_happened.emit(dialogue)
|
||||||
|
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
|
||||||
if distance > interactionDistance:
|
|
||||||
return []
|
|
||||||
return [{
|
|
||||||
"id": "npc:%s" % get_instance_id(),
|
|
||||||
"title": "与 %s 交谈" % npcName,
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 40,
|
|
||||||
"callback": Callable(self, "interact"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
# 在 NPC 头顶生成一次性聊天气泡。
|
# 在 NPC 头顶生成一次性聊天气泡。
|
||||||
#
|
#
|
||||||
# 参数:
|
# 参数:
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const CAFE_NAME_LABEL_VISUAL_MAX_WIDTH: int = 118
|
|||||||
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
||||||
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
||||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
const DIRECTION_ROWS: Dictionary = {
|
const DIRECTION_ROWS: Dictionary = {
|
||||||
"down": 0,
|
"down": 0,
|
||||||
"up": 1,
|
"up": 1,
|
||||||
@@ -37,9 +36,12 @@ const DIRECTION_ROWS: Dictionary = {
|
|||||||
@onready var sprite: Sprite2D = $Sprite2D
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
var _nameLabel: Label
|
var _nameLabel: Label
|
||||||
var _cafeCompanionTarget: CafeCompanionTarget
|
var _cafeCompanionTarget: CafeCompanionTarget
|
||||||
|
var _interactable: InteractableComponent
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("whaletown_interactable")
|
_interactable = InteractableComponent.new()
|
||||||
|
_interactable.interaction_distance = 160.0
|
||||||
|
add_child(_interactable)
|
||||||
# 初始化时确保无物理处理
|
# 初始化时确保无物理处理
|
||||||
set_physics_process(false)
|
set_physics_process(false)
|
||||||
# 初始位置设为当前位置
|
# 初始位置设为当前位置
|
||||||
@@ -54,37 +56,16 @@ func _ready() -> void:
|
|||||||
if has_node("CollisionShape2D"):
|
if has_node("CollisionShape2D"):
|
||||||
$CollisionShape2D.disabled = true
|
$CollisionShape2D.disabled = true
|
||||||
|
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
func is_interaction_active(_component: InteractableComponent) -> bool:
|
||||||
if userId.strip_edges().is_empty() or player == null:
|
return not userId.strip_edges().is_empty()
|
||||||
return []
|
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]:
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
var actions: Array[InteractionAction] = []
|
||||||
if distance > 160.0:
|
|
||||||
return []
|
|
||||||
var display_name := username if not username.strip_edges().is_empty() else "玩家"
|
var display_name := username if not username.strip_edges().is_empty() else "玩家"
|
||||||
return [
|
actions.append(InteractionAction.create("player_card:%s" % userId, "查看 %s 的社区名片" % display_name, 60, Callable(self, "_show_community_profile")))
|
||||||
{
|
actions.append(InteractionAction.create("player_dm:%s" % userId, "私聊 %s" % display_name, 61, Callable(self, "_open_private_chat")))
|
||||||
"id": "player_card:%s" % userId,
|
actions.append(InteractionAction.create("player_friend:%s" % userId, "申请添加 %s 为好友" % display_name, 62, Callable(self, "_request_friend")))
|
||||||
"title": "查看 %s 的社区名片" % display_name,
|
return actions
|
||||||
"distance": distance,
|
|
||||||
"priority": 60,
|
|
||||||
"callback": Callable(self, "_show_community_profile"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "player_dm:%s" % userId,
|
|
||||||
"title": "私聊 %s" % display_name,
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 61,
|
|
||||||
"callback": Callable(self, "_open_private_chat"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "player_friend:%s" % userId,
|
|
||||||
"title": "申请添加 %s 为好友" % display_name,
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 62,
|
|
||||||
"callback": Callable(self, "_request_friend"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
func _show_community_profile() -> void:
|
func _show_community_profile() -> void:
|
||||||
var socialManager := get_node_or_null("/root/SocialManager")
|
var socialManager := get_node_or_null("/root/SocialManager")
|
||||||
|
|||||||
@@ -4,25 +4,17 @@ class_name DatawhaleHonorBoard
|
|||||||
const RANKING_PANEL_SCENE: PackedScene = preload("res://scenes/ui/datawhale_honor_ranking_panel.tscn")
|
const RANKING_PANEL_SCENE: PackedScene = preload("res://scenes/ui/datawhale_honor_ranking_panel.tscn")
|
||||||
const RANKING_PANEL_NAME: String = "DatawhaleHonorRankingPanel"
|
const RANKING_PANEL_NAME: String = "DatawhaleHonorRankingPanel"
|
||||||
const INTERACTION_COLLISION_LAYER: int = 2
|
const INTERACTION_COLLISION_LAYER: int = 2
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
collision_layer = INTERACTION_COLLISION_LAYER
|
collision_layer = INTERACTION_COLLISION_LAYER
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
add_to_group("whaletown_interactable")
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_id = "honor_board:%s" % str(get_path())
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
interactable.interaction_title = "查看荣誉榜"
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
interactable.interaction_priority = 35
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
interactable.interaction_distance = 150.0
|
||||||
if distance > 150.0:
|
interactable.activation_method = &"interact"
|
||||||
return []
|
add_child(interactable)
|
||||||
return [{
|
|
||||||
"id": "honor_board:%s" % get_instance_id(),
|
|
||||||
"title": "查看荣誉榜",
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 35,
|
|
||||||
"callback": Callable(self, "interact"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
var root: Window = get_tree().root
|
var root: Window = get_tree().root
|
||||||
|
|||||||
@@ -4,25 +4,17 @@ class_name NoticeBoard
|
|||||||
const NOTICE_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/notice_dialog.tscn")
|
const NOTICE_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/notice_dialog.tscn")
|
||||||
const NOTICE_DIALOG_NAME: String = "NoticeDialog"
|
const NOTICE_DIALOG_NAME: String = "NoticeDialog"
|
||||||
const INTERACTION_COLLISION_LAYER: int = 2
|
const INTERACTION_COLLISION_LAYER: int = 2
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
collision_layer = INTERACTION_COLLISION_LAYER
|
collision_layer = INTERACTION_COLLISION_LAYER
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
add_to_group("whaletown_interactable")
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_id = "notice_board:%s" % str(get_path())
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
interactable.interaction_title = "查看公告栏"
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
interactable.interaction_priority = 35
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
interactable.interaction_distance = 150.0
|
||||||
if distance > 150.0:
|
interactable.activation_method = &"interact"
|
||||||
return []
|
add_child(interactable)
|
||||||
return [{
|
|
||||||
"id": "notice_board:%s" % get_instance_id(),
|
|
||||||
"title": "查看公告栏",
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 35,
|
|
||||||
"callback": Callable(self, "interact"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
var root: Window = get_tree().root
|
var root: Window = get_tree().root
|
||||||
|
|||||||
@@ -4,25 +4,17 @@ class_name WelcomeBoard
|
|||||||
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
|
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
|
||||||
const WELCOME_DIALOG_NAME: String = "WelcomeDialog"
|
const WELCOME_DIALOG_NAME: String = "WelcomeDialog"
|
||||||
const INTERACTION_COLLISION_LAYER: int = 2
|
const INTERACTION_COLLISION_LAYER: int = 2
|
||||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
collision_layer = INTERACTION_COLLISION_LAYER
|
collision_layer = INTERACTION_COLLISION_LAYER
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
add_to_group("whaletown_interactable")
|
var interactable: InteractableComponent = InteractableComponent.new()
|
||||||
|
interactable.interaction_id = "welcome_board:%s" % str(get_path())
|
||||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
interactable.interaction_title = "查看新人引导"
|
||||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
interactable.interaction_priority = 35
|
||||||
var distance: float = anchor_position.distance_to(player.global_position)
|
interactable.interaction_distance = 150.0
|
||||||
if distance > 150.0:
|
interactable.activation_method = &"interact"
|
||||||
return []
|
add_child(interactable)
|
||||||
return [{
|
|
||||||
"id": "welcome_board:%s" % get_instance_id(),
|
|
||||||
"title": "查看新人引导",
|
|
||||||
"distance": distance,
|
|
||||||
"priority": 35,
|
|
||||||
"callback": Callable(self, "interact"),
|
|
||||||
}]
|
|
||||||
|
|
||||||
func interact() -> void:
|
func interact() -> void:
|
||||||
var root: Window = get_tree().root
|
var root: Window = get_tree().root
|
||||||
|
|||||||
Reference in New Issue
Block a user