feat: add nearby social interactions
This commit is contained in:
@@ -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 头顶生成一次性聊天气泡。
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user