using UnityEngine;
using BaseGames.World;
using BaseGames.Localization;
namespace BaseGames.Dialogue
{
///
/// 可交互 NPC 基类(架构 14_NarrativeModule §5)。
/// 实现 IInteractable,触发对话序列;子类(如 QuestGiver)可覆盖对话选择逻辑。
/// 程序集: BaseGames.Dialogue
///
public class InteractableNPC : MonoBehaviour, IInteractable
{
[Header("NPC 基础")]
[Tooltip("NPC 唯一 ID(如 \"NPC_Elder\")。对话结束时随 EVT_NpcDialogueCompleted 广播,用于驱动对话类任务目标进度。\n" +
"需与 QuestSO 目标中 targetNpcId 保持一致。")]
[SerializeField] protected string _npcId;
[Tooltip("默认对话序列。无其他逻辑覆盖时播放此序列。NarrativeNPC/QuestGiver 子类通过 GetCurrentDialogue() 返回更精确的版本。")]
[SerializeField] protected DialogueSequenceSO _defaultDialogue;
[Tooltip("玩家进入此半径(单位:Unity 单位)后显示交互提示。建议 1.0–2.0。\n编辑器下在场景视图中以黄色圆圈可视化。")]
[SerializeField] protected float _interactRadius = 1.5f;
[Tooltip("交互提示本地化 Key(如 \"INTERACT_Talk\")。运行时通过 LocalizationManager 解析为实际文字。\n" +
"留空时回退到内置字符串 \"对话\"。")]
[SerializeField] protected string _interactPromptKey = "INTERACT_Talk";
[Header("范围检测")]
[Tooltip("玩家所在的物理层。OnTriggerEnter2D / OnTriggerExit2D 仅响应属于此层的碰撞体,\n" +
"实现 NPC 自包含的交互范围检测,无需外部 PlayerInteractionDetector 组件。\n" +
"将玩家 GameObject 的 Layer 与此 Mask 对齐即可(推荐专用 \"Player\" 层)。\n" +
"若留空(值为 0),则跳过层级过滤,任意碰撞体均可触发(调试用,不推荐上线)。")]
[SerializeField] protected LayerMask _playerLayer;
// ── IInteractable ──────────────────────────────────────────────────
public virtual bool CanInteract => true;
public virtual string InteractPrompt
{
get
{
if (!string.IsNullOrEmpty(_interactPromptKey))
{
var resolved = LocalizationManager.Get(_interactPromptKey, "UI");
if (!string.IsNullOrEmpty(resolved)) return resolved;
}
return "对话";
}
}
// ── 范围进出通知(供子组件订阅,如 InteractionPromptController)──────
/// 玩家进入交互范围时触发。参数为玩家 Transform。
public event System.Action PlayerEnteredRange;
/// 玩家离开交互范围时触发。
public event System.Action PlayerExitedRange;
public void Interact(Transform player)
{
if (!CanInteract) return;
Interact_Internal(player);
var dialogue = GetCurrentDialogue();
if (dialogue != null)
PlayDialogue(dialogue, player);
}
public virtual void OnPlayerEnterRange(Transform player) { PlayerEnteredRange?.Invoke(player); }
public virtual void OnPlayerExitRange() { PlayerExitedRange?.Invoke(); }
// ── 自包含物理范围检测 ─────────────────────────────────────────────
// 需在 NPC Prefab 上挂载 Collider2D(设为 IsTrigger),并将 Collider2D.size/radius
// 配置为期望的交互半径。OnTriggerEnter2D / Exit2D 会自动过滤非玩家碰撞体。
private void OnTriggerEnter2D(Collider2D other)
{
if (_playerLayer.value != 0 && ((1 << other.gameObject.layer) & _playerLayer.value) == 0) return;
OnPlayerEnterRange(other.transform);
}
private void OnTriggerExit2D(Collider2D other)
{
if (_playerLayer.value != 0 && ((1 << other.gameObject.layer) & _playerLayer.value) == 0) return;
OnPlayerExitRange();
}
// ── 子类覆盖点 ──────────────────────────────────────────────────────
/// 交互前置逻辑(如任务接收/完成判断)。子类覆盖此方法。
protected virtual void Interact_Internal(Transform player) { }
/// 返回当前应播放的对话序列。子类根据任务状态返回不同版本。
protected virtual DialogueSequenceSO GetCurrentDialogue() => _defaultDialogue;
// ── 对话播放 ───────────────────────────────────────────────────────
protected virtual void PlayDialogue(DialogueSequenceSO sequence, Transform player)
{
if (sequence == null) return;
var manager = BaseGames.Core.ServiceLocator.GetOrDefault();
if (manager == null)
{
Debug.LogWarning($"[InteractableNPC:{_npcId}] DialogueManager 未注册到 ServiceLocator,无法播放对话。");
return;
}
manager.StartDialogue(sequence, _npcId);
}
// ── 编辑器辅助 ────────────────────────────────────────────────────
// 注意:OnValidate 声明在 #if 外,确保子类在非编辑器构建中调用 base.OnValidate() 不会编译失败。
protected virtual void OnValidate()
{
#if UNITY_EDITOR
if (_playerLayer.value == 0)
Debug.LogWarning(
$"[InteractableNPC:{name}] _playerLayer 未设置(value=0)。" +
"OnTriggerEnter2D 将响应所有层,建议在 Inspector 中指定玩家所在层。", this);
// 检测 _interactRadius 与 CircleCollider2D.radius 是否同步(仅输出一次,非逐帧)
var circle = GetComponent();
if (circle != null && !Mathf.Approximately(circle.radius, _interactRadius))
Debug.LogWarning(
$"[InteractableNPC:{name}] _interactRadius({_interactRadius:F2}) 与 " +
$"CircleCollider2D.radius({circle.radius:F2}) 不一致," +
"交互范围视觉(Gizmos)与物理碰撞可能不匹配,请手动对齐。", this);
#endif
}
#if UNITY_EDITOR
protected virtual void OnDrawGizmosSelected()
{
// Collider2D 不一致时改绘红色(警告已在 OnValidate 中输出,此处不重复 LogWarning)
bool mismatch = false;
var circle = GetComponent();
if (circle != null && !Mathf.Approximately(circle.radius, _interactRadius))
mismatch = true;
UnityEditor.Handles.color = mismatch
? new Color(1f, 0.2f, 0.2f, 0.8f)
: new Color(1f, 0.92f, 0.016f, 0.6f);
UnityEditor.Handles.DrawWireDisc(transform.position, Vector3.forward, _interactRadius);
}
#endif
}
}