From 74a0fb830956350b726fe8339bf037fc2f6edd70 Mon Sep 17 00:00:00 2001 From: ANG-Server <96008766+ANGJustinl@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:23:26 +0800 Subject: [PATCH] refactor: unify interactable components --- _Core/interactions/InteractableComponent.gd | 96 +++++++++++++++++++ .../interactions/InteractableComponent.gd.uid | 1 + _Core/interactions/InteractionAction.gd | 20 ++++ _Core/interactions/InteractionAction.gd.uid | 1 + _Core/managers/InteractionManager.gd | 72 ++++++-------- _Core/utils/InteractionAnchor.gd | 14 --- _Core/utils/InteractionAnchor.gd.uid | 1 - scenes/Maps/CafeInterior.gd | 42 ++++---- scenes/Maps/ScenePortal.gd | 29 ++---- scenes/Maps/WorkZone.gd | 1 - scenes/Maps/WorkZoneBuildingArea.gd | 25 +++-- scenes/characters/CafeCompanionTarget.gd | 13 +++ scenes/characters/NPCController.gd | 23 ++--- scenes/characters/RemotePlayer.gd | 45 +++------ scenes/prefabs/items/DatawhaleHonorBoard.gd | 22 ++--- scenes/prefabs/items/NoticeBoard.gd | 22 ++--- scenes/prefabs/items/WelcomeBoard.gd | 22 ++--- 17 files changed, 241 insertions(+), 208 deletions(-) create mode 100644 _Core/interactions/InteractableComponent.gd create mode 100644 _Core/interactions/InteractableComponent.gd.uid create mode 100644 _Core/interactions/InteractionAction.gd create mode 100644 _Core/interactions/InteractionAction.gd.uid delete mode 100644 _Core/utils/InteractionAnchor.gd delete mode 100644 _Core/utils/InteractionAnchor.gd.uid diff --git a/_Core/interactions/InteractableComponent.gd b/_Core/interactions/InteractableComponent.gd new file mode 100644 index 0000000..e0b99dd --- /dev/null +++ b/_Core/interactions/InteractableComponent.gd @@ -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 diff --git a/_Core/interactions/InteractableComponent.gd.uid b/_Core/interactions/InteractableComponent.gd.uid new file mode 100644 index 0000000..ea4ce62 --- /dev/null +++ b/_Core/interactions/InteractableComponent.gd.uid @@ -0,0 +1 @@ +uid://cn3fccr0qc41h diff --git a/_Core/interactions/InteractionAction.gd b/_Core/interactions/InteractionAction.gd new file mode 100644 index 0000000..ad3f3ab --- /dev/null +++ b/_Core/interactions/InteractionAction.gd @@ -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() diff --git a/_Core/interactions/InteractionAction.gd.uid b/_Core/interactions/InteractionAction.gd.uid new file mode 100644 index 0000000..18161a3 --- /dev/null +++ b/_Core/interactions/InteractionAction.gd.uid @@ -0,0 +1 @@ +uid://dbrui8h3sbjrn diff --git a/_Core/managers/InteractionManager.gd b/_Core/managers/InteractionManager.gd index 4d6798b..1341642 100644 --- a/_Core/managers/InteractionManager.gd +++ b/_Core/managers/InteractionManager.gd @@ -6,9 +6,8 @@ const MAX_ACTIONS_VISIBLE: int = 5 const ACCENT: Color = Color("58c7db") 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 InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") -var _actions: Array[Dictionary] = [] +var _actions: Array[InteractionAction] = [] var _selected_index: int = 0 var _last_selected_id: String = "" var _scan_elapsed: float = SCAN_INTERVAL @@ -58,32 +57,23 @@ func _refresh_actions() -> void: if player == null: _set_actions([]) return - var collected: Array[Dictionary] = [] - for node in get_tree().get_nodes_in_group("whaletown_interactable"): - if not is_instance_valid(node) or not node.has_method("get_interaction_actions"): + var collected: Array[InteractionAction] = [] + for node: Node in get_tree().get_nodes_in_group(InteractableComponent.GROUP): + var interactable: InteractableComponent = node as InteractableComponent + if interactable == null: continue - var actions_variant: Variant = node.call("get_interaction_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.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: - var priority_a := int(a.get("priority", 100)) - var priority_b := int(b.get("priority", 100)) - if priority_a != priority_b: - return priority_a < priority_b - var distance_a := float(a.get("distance", INF)) - 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", "")) + for action: InteractionAction in interactable.get_actions(player): + collected.append(action) + collected.sort_custom(func(a: InteractionAction, b: InteractionAction) -> bool: + if a.priority != b.priority: + return a.priority < b.priority + if not is_equal_approx(a.distance, b.distance): + return a.distance < b.distance + return a.title < b.title ) _set_actions(collected) -func _set_actions(next_actions: Array[Dictionary]) -> void: +func _set_actions(next_actions: Array[InteractionAction]) -> void: _actions = next_actions if _actions.is_empty(): _selected_index = 0 @@ -92,30 +82,29 @@ func _set_actions(next_actions: Array[Dictionary]) -> void: return var matched_index := -1 for index in _actions.size(): - if str(_actions[index].get("id", "")) == _last_selected_id: + if _actions[index].id == _last_selected_id: matched_index = index break _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() func _select_offset(offset: int) -> void: if _actions.is_empty(): return _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() func _execute_selected() -> void: if _executing or _actions.is_empty(): return - var action := _actions[_selected_index] - var callback_variant: Variant = action.get("callback", Callable()) - if not (callback_variant is Callable) or not (callback_variant as Callable).is_valid(): + var action: InteractionAction = _actions[_selected_index] + if not action.activate.is_valid(): return _executing = true _render() - (callback_variant as Callable).call() + action.activate.call() await get_tree().create_timer(0.18).timeout _executing = false _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 finish: int = mini(_actions.size(), start + MAX_ACTIONS_VISIBLE) for index in range(start, finish): - var action := _actions[index] + var action: InteractionAction = _actions[index] var row := Label.new() row.custom_minimum_size = Vector2(0, 29) row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER row.add_theme_font_size_override("font_size", 16) 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)) if selected: row.add_theme_stylebox_override("normal", _selected_style()) @@ -191,19 +180,12 @@ func _render_interaction_points() -> void: if markers == null: return var positions: Array[Vector2] = [] - for node: Node in get_tree().get_nodes_in_group("whaletown_interactable"): - if not is_instance_valid(node) or not node.has_method("get_interaction_actions"): + for node: Node in get_tree().get_nodes_in_group(InteractableComponent.GROUP): + var interactable: InteractableComponent = node as InteractableComponent + if interactable == null: continue - if node.has_method("get_interaction_marker_positions"): - var marker_positions_variant: Variant = node.call("get_interaction_marker_positions") - 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)) + for position: Vector2 in interactable.get_marker_positions(): + positions.append(position) markers.call("set_points", positions) func _ensure_point_markers() -> Node2D: diff --git a/_Core/utils/InteractionAnchor.gd b/_Core/utils/InteractionAnchor.gd deleted file mode 100644 index affc423..0000000 --- a/_Core/utils/InteractionAnchor.gd +++ /dev/null @@ -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 diff --git a/_Core/utils/InteractionAnchor.gd.uid b/_Core/utils/InteractionAnchor.gd.uid deleted file mode 100644 index ed9b36f..0000000 --- a/_Core/utils/InteractionAnchor.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dogltsticcpy2 diff --git a/scenes/Maps/CafeInterior.gd b/scenes/Maps/CafeInterior.gd index a9cce9a..1615300 100644 --- a/scenes/Maps/CafeInterior.gd +++ b/scenes/Maps/CafeInterior.gd @@ -23,7 +23,6 @@ const COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH: int = 118 const COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12 const NPC_NAMEPLATE_OFFSET_Y: float = -136.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 playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D @@ -39,7 +38,6 @@ var _isChangingScene: bool = false var _lastRecruitmentClickMsec: int = 0 func _ready() -> void: - add_to_group("whaletown_interactable") _align_service_occupants() _configure_static_companion_nameplates() _apply_spawn_point() @@ -47,6 +45,7 @@ func _ready() -> void: _connect_exit_area() _connect_recruitment_area() _connect_cafe_companion_events() + _register_interactables() _discover_destination() 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_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: 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: if _isChangingScene: return diff --git a/scenes/Maps/ScenePortal.gd b/scenes/Maps/ScenePortal.gd index 8b2e2b4..ca3e743 100644 --- a/scenes/Maps/ScenePortal.gd +++ b/scenes/Maps/ScenePortal.gd @@ -1,8 +1,6 @@ class_name ScenePortal extends Area2D -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") - # ============================================================================ # ScenePortal.gd - 场景入口传送组件 # ============================================================================ @@ -16,27 +14,20 @@ const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") @export var interactionDistance: float = 160.0 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: 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: if targetSceneName.is_empty(): push_warning("ScenePortal: targetSceneName is empty.") diff --git a/scenes/Maps/WorkZone.gd b/scenes/Maps/WorkZone.gd index f721b60..dedf552 100644 --- a/scenes/Maps/WorkZone.gd +++ b/scenes/Maps/WorkZone.gd @@ -31,7 +31,6 @@ var _isInsideMall: bool = false var _mallCanEnter: bool = true func _ready() -> void: - add_to_group("whaletown_interactable") _apply_spawn_point() _configure_camera() _ensure_mall_panel() diff --git a/scenes/Maps/WorkZoneBuildingArea.gd b/scenes/Maps/WorkZoneBuildingArea.gd index b882bb5..37c82b8 100644 --- a/scenes/Maps/WorkZoneBuildingArea.gd +++ b/scenes/Maps/WorkZoneBuildingArea.gd @@ -8,7 +8,6 @@ extends Area2D # ============================================================================ const INTERACTION_COLLISION_LAYER: int = 2 -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") @export var buildingId: String = "" @export var buildingTitle: String = "" @@ -18,20 +17,18 @@ const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") func _ready() -> void: collision_layer = INTERACTION_COLLISION_LAYER 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]: - var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self) - var distance: float = anchor_position.distance_to(player.global_position) - if distance > interactionDistance: - return [] - return [{ - "id": "building:%s" % buildingId, - "title": "使用 %s" % (buildingTitle if not buildingTitle.is_empty() else "设施"), - "distance": distance, - "priority": 30, - "callback": Callable(self, "interact"), - }] +func is_interaction_active(_component: InteractableComponent) -> bool: + return not buildingId.strip_edges().is_empty() + +func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]: + var actions: Array[InteractionAction] = [] + var title: String = buildingTitle if not buildingTitle.is_empty() else "设施" + actions.append(InteractionAction.create("building:%s" % buildingId, "使用 %s" % title, 30, Callable(self, "interact"))) + return actions func interact() -> void: var payload := { diff --git a/scenes/characters/CafeCompanionTarget.gd b/scenes/characters/CafeCompanionTarget.gd index 673753f..4f3f43f 100644 --- a/scenes/characters/CafeCompanionTarget.gd +++ b/scenes/characters/CafeCompanionTarget.gd @@ -19,9 +19,22 @@ class_name CafeCompanionTarget var _lastClickMsec: int = 0 func _ready() -> void: + var interactable: InteractableComponent = InteractableComponent.new() + interactable.interaction_distance = 160.0 + add_child(interactable) input_pickable = 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: if not (event is InputEventMouseButton): return diff --git a/scenes/characters/NPCController.gd b/scenes/characters/NPCController.gd index 6c28d8a..78048d8 100644 --- a/scenes/characters/NPCController.gd +++ b/scenes/characters/NPCController.gd @@ -18,7 +18,6 @@ class_name NPCController signal interaction_happened(text: String) 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_TARGET_OFFSET: Vector2 = Vector2(0, -84) const NPC_TALKED_EVENT: String = "npc_talked" @@ -42,7 +41,14 @@ const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12 var _nameplate: Label 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 可以复用同一个控制器。 if animation_player.has_animation("idle"): animation_player.play("idle") @@ -68,19 +74,6 @@ func interact() -> void: }) 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 头顶生成一次性聊天气泡。 # # 参数: diff --git a/scenes/characters/RemotePlayer.gd b/scenes/characters/RemotePlayer.gd index 044f563..905a41c 100644 --- a/scenes/characters/RemotePlayer.gd +++ b/scenes/characters/RemotePlayer.gd @@ -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_OFFSET_Y: float = -96.0 const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres") -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") const DIRECTION_ROWS: Dictionary = { "down": 0, "up": 1, @@ -37,9 +36,12 @@ const DIRECTION_ROWS: Dictionary = { @onready var sprite: Sprite2D = $Sprite2D var _nameLabel: Label var _cafeCompanionTarget: CafeCompanionTarget +var _interactable: InteractableComponent func _ready() -> void: - add_to_group("whaletown_interactable") + _interactable = InteractableComponent.new() + _interactable.interaction_distance = 160.0 + add_child(_interactable) # 初始化时确保无物理处理 set_physics_process(false) # 初始位置设为当前位置 @@ -54,37 +56,16 @@ func _ready() -> void: if has_node("CollisionShape2D"): $CollisionShape2D.disabled = true -func get_interaction_actions(player: Node2D) -> Array[Dictionary]: - if userId.strip_edges().is_empty() or player == null: - return [] - var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self) - var distance: float = anchor_position.distance_to(player.global_position) - if distance > 160.0: - return [] +func is_interaction_active(_component: InteractableComponent) -> bool: + return not userId.strip_edges().is_empty() + +func build_interaction_actions(_component: InteractableComponent, _player: Node2D) -> Array[InteractionAction]: + var actions: Array[InteractionAction] = [] var display_name := username if not username.strip_edges().is_empty() else "玩家" - return [ - { - "id": "player_card:%s" % userId, - "title": "查看 %s 的社区名片" % display_name, - "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"), - }, - ] + 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"))) + actions.append(InteractionAction.create("player_friend:%s" % userId, "申请添加 %s 为好友" % display_name, 62, Callable(self, "_request_friend"))) + return actions func _show_community_profile() -> void: var socialManager := get_node_or_null("/root/SocialManager") diff --git a/scenes/prefabs/items/DatawhaleHonorBoard.gd b/scenes/prefabs/items/DatawhaleHonorBoard.gd index c59359b..afe3ac4 100644 --- a/scenes/prefabs/items/DatawhaleHonorBoard.gd +++ b/scenes/prefabs/items/DatawhaleHonorBoard.gd @@ -4,25 +4,17 @@ class_name DatawhaleHonorBoard const RANKING_PANEL_SCENE: PackedScene = preload("res://scenes/ui/datawhale_honor_ranking_panel.tscn") const RANKING_PANEL_NAME: String = "DatawhaleHonorRankingPanel" const INTERACTION_COLLISION_LAYER: int = 2 -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") func _ready() -> void: collision_layer = INTERACTION_COLLISION_LAYER collision_mask = 0 - add_to_group("whaletown_interactable") - -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 > 150.0: - return [] - return [{ - "id": "honor_board:%s" % get_instance_id(), - "title": "查看荣誉榜", - "distance": distance, - "priority": 35, - "callback": Callable(self, "interact"), - }] + var interactable: InteractableComponent = InteractableComponent.new() + interactable.interaction_id = "honor_board:%s" % str(get_path()) + interactable.interaction_title = "查看荣誉榜" + interactable.interaction_priority = 35 + interactable.interaction_distance = 150.0 + interactable.activation_method = &"interact" + add_child(interactable) func interact() -> void: var root: Window = get_tree().root diff --git a/scenes/prefabs/items/NoticeBoard.gd b/scenes/prefabs/items/NoticeBoard.gd index ad2c9d3..ff7c7bd 100644 --- a/scenes/prefabs/items/NoticeBoard.gd +++ b/scenes/prefabs/items/NoticeBoard.gd @@ -4,25 +4,17 @@ class_name NoticeBoard const NOTICE_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/notice_dialog.tscn") const NOTICE_DIALOG_NAME: String = "NoticeDialog" const INTERACTION_COLLISION_LAYER: int = 2 -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") func _ready() -> void: collision_layer = INTERACTION_COLLISION_LAYER collision_mask = 0 - add_to_group("whaletown_interactable") - -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 > 150.0: - return [] - return [{ - "id": "notice_board:%s" % get_instance_id(), - "title": "查看公告栏", - "distance": distance, - "priority": 35, - "callback": Callable(self, "interact"), - }] + var interactable: InteractableComponent = InteractableComponent.new() + interactable.interaction_id = "notice_board:%s" % str(get_path()) + interactable.interaction_title = "查看公告栏" + interactable.interaction_priority = 35 + interactable.interaction_distance = 150.0 + interactable.activation_method = &"interact" + add_child(interactable) func interact() -> void: var root: Window = get_tree().root diff --git a/scenes/prefabs/items/WelcomeBoard.gd b/scenes/prefabs/items/WelcomeBoard.gd index cd08b59..09176d5 100644 --- a/scenes/prefabs/items/WelcomeBoard.gd +++ b/scenes/prefabs/items/WelcomeBoard.gd @@ -4,25 +4,17 @@ class_name WelcomeBoard const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn") const WELCOME_DIALOG_NAME: String = "WelcomeDialog" const INTERACTION_COLLISION_LAYER: int = 2 -const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd") func _ready() -> void: collision_layer = INTERACTION_COLLISION_LAYER collision_mask = 0 - add_to_group("whaletown_interactable") - -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 > 150.0: - return [] - return [{ - "id": "welcome_board:%s" % get_instance_id(), - "title": "查看新人引导", - "distance": distance, - "priority": 35, - "callback": Callable(self, "interact"), - }] + var interactable: InteractableComponent = InteractableComponent.new() + interactable.interaction_id = "welcome_board:%s" % str(get_path()) + interactable.interaction_title = "查看新人引导" + interactable.interaction_priority = 35 + interactable.interaction_distance = 150.0 + interactable.activation_method = &"interact" + add_child(interactable) func interact() -> void: var root: Window = get_tree().root