feat: add nearby social interactions

This commit is contained in:
ANG-Server
2026-07-21 22:19:41 +08:00
parent 0bab768b14
commit 9dee47492c
43 changed files with 1499 additions and 77 deletions

View File

@@ -18,6 +18,7 @@ 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"
@@ -34,12 +35,14 @@ const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
@export_multiline var dialogue: String = "欢迎来到WhaleTown我是镇长范鲸晶"
@export var showNameplate: bool = false
@export var nameplateOffsetY: float = -112.0
@export var interactionDistance: float = 150.0
@onready var animation_player: AnimationPlayer = $AnimationPlayer
var _nameplate: Label
func _ready() -> void:
add_to_group("whaletown_interactable")
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
if animation_player.has_animation("idle"):
animation_player.play("idle")
@@ -63,7 +66,20 @@ func interact() -> void:
"npc_name": npcName,
"dialogue": dialogue
})
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 头顶生成一次性聊天气泡。
#

View File

@@ -25,8 +25,11 @@ const DIRECTION_ROWS: Dictionary = {
var lastDirection: String = "down"
var _nameLabel: Label
var _movementLocked: bool = false
var has_scene_position_override: bool = false
var _discoveryElapsed: float = 0.0
func _ready() -> void:
add_to_group("whaletown_local_player")
_reset_movement_input_state()
_apply_current_appearance()
_subscribe_to_appearance_events()
@@ -71,28 +74,40 @@ func _release_movement_actions() -> void:
func _check_spawn_position() -> void:
var spawnPos: Variant = SceneManager.get_next_scene_position()
if spawnPos != null:
if spawnPos is Vector2:
global_position = spawnPos
_update_world_sort_z()
has_scene_position_override = true
_update_world_sort_z()
func _physics_process(delta: float) -> void:
_handle_movement(delta)
_update_world_sort_z()
_handle_interaction()
_report_nearby_discoveries(delta)
func _handle_interaction() -> void:
if _is_text_input_focused():
# 统一交互由 /root/InteractionManager 收集附近候选并处理 E 键。
return
func _report_nearby_discoveries(delta: float) -> void:
_discoveryElapsed += delta
if _discoveryElapsed < 0.35:
return
if Input.is_action_just_pressed("interact"):
EventSystem.emit_event(EventNames.INTERACT_PRESSED, {
"player": self,
"position": global_position,
"direction": lastDirection
})
if ray_cast.is_colliding():
var collider := ray_cast.get_collider()
if collider and collider.has_method("interact"):
collider.interact()
_discoveryElapsed = 0.0
var scene := get_tree().current_scene
if scene == null:
return
var map_id: String = str({
"Square": "whale_port",
"WorkZone": "work_zone",
"CafeInterior": "whale_cafe",
"PersonalSpace": "personal_space",
}.get(str(scene.name), ""))
if map_id.is_empty():
return
var social_manager := get_node_or_null("/root/SocialManager")
if social_manager != null and social_manager.has_method("discover_nearby_destinations"):
social_manager.call("discover_nearby_destinations", map_id, global_position)
func _handle_movement(_delta: float) -> void:
if _movementLocked:
@@ -111,10 +126,7 @@ func _handle_movement(_delta: float) -> void:
return
# 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md)
var direction := Input.get_vector(
"move_left", "move_right",
"move_up", "move_down"
)
var direction := _movement_direction()
# 应用移动
if direction != Vector2.ZERO:
@@ -133,6 +145,16 @@ func _handle_movement(_delta: float) -> void:
"position": global_position
})
func _movement_direction() -> Vector2:
var interactionManager := get_node_or_null("/root/InteractionManager")
if interactionManager != null and interactionManager.has_method("is_selection_active") and bool(interactionManager.call("is_selection_active")):
# 有交互候选时方向键用于选择,保留 WASD 移动。
return Vector2(
(-1.0 if Input.is_key_pressed(KEY_A) else 0.0) + (1.0 if Input.is_key_pressed(KEY_D) else 0.0),
(-1.0 if Input.is_key_pressed(KEY_W) else 0.0) + (1.0 if Input.is_key_pressed(KEY_S) else 0.0)
).normalized()
return Input.get_vector("move_left", "move_right", "move_up", "move_down")
func _update_animation_state(direction: Vector2) -> void:
if not animation_player:
return

View File

@@ -26,6 +26,7 @@ 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,
@@ -38,6 +39,7 @@ var _nameLabel: Label
var _cafeCompanionTarget: CafeCompanionTarget
func _ready() -> void:
add_to_group("whaletown_interactable")
# 初始化时确保无物理处理
set_physics_process(false)
# 初始位置设为当前位置
@@ -52,6 +54,53 @@ 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 []
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"),
},
]
func _show_community_profile() -> void:
var socialManager := get_node_or_null("/root/SocialManager")
if socialManager != null and socialManager.has_method("show_profile"):
socialManager.call("show_profile", userId)
func _open_private_chat() -> void:
var socialManager := get_node_or_null("/root/SocialManager")
if socialManager != null and socialManager.has_method("open_private_chat"):
socialManager.call("open_private_chat", userId, username)
func _request_friend() -> void:
var socialManager := get_node_or_null("/root/SocialManager")
if socialManager != null and socialManager.has_method("request_friend"):
socialManager.call("request_friend", userId, username)
func _exit_tree() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem != null: