using UnityEngine; namespace BaseGames.Dialogue { /// /// NPC 元数据资产(架构 14_NarrativeModule §2)。 /// 将 NPC 的唯一 ID、本地化名称 Key、头像、好感度上限集中在一处管理。 /// /// 关联: /// • 通过 _npcId 字段与此 SO 对应。 /// • 管理对话 UI 侧头像/颜色(二者可共享同一 Sprite,或独立维护)。 /// • giverNpc 字段直接引用此 SO,避免手填字符串。 /// /// 资产路径:Assets/_Game/Data/NPC/NPC_{npcId}.asset /// [CreateAssetMenu(menuName = "BaseGames/NPC/NPC")] public class NpcSO : ScriptableObject { [Header("标识")] [Tooltip("NPC 唯一 ID,如 \"NPC_Elder\"。需与 InteractableNPC._npcId、QuestSO.giverNpcId 保持一致。")] public string npcId; [Header("显示")] [Tooltip("本地化 Key,如 \"NPC_Elder_Name\"。通过 LocalizationManager 解析为实际名称。")] public string nameKey; [Tooltip("NPC 头像,用于地图、任务日志、DataHub 等 UI。")] public Sprite portrait; [Header("好感度")] [Tooltip("该 NPC 的好感度上限(0 = 无上限)。\n" + "QuestManager.CompleteQuest 发放 affinityBonus 时,不超过此数值。\n" + "UI 侧可用此值绘制好感度进度条满格。")] [Min(0)] public int maxAffinity = 0; [Header("本地化")] [Tooltip("nameKey 所在的本地化表名(默认 \"UI\")。\n" + "若 NPC 名称存储在非默认表(如 \"Character\"),在此修改后 NpcSOEditor 预览和跳转按钮将使用正确的表。")] public string localizationTable = "UI"; [Header("交互提示")] [Tooltip("与此 NPC 交互时显示的提示本地化 Key(如 \"INTERACT_Talk\")。\n" + "留空时 InteractableNPC 回退到内置字符串 \"对话\"。")] public string interactPromptKey = "INTERACT_Talk"; #if UNITY_EDITOR // npcId → 资产路径,5 秒 TTL,跨所有 NpcSO.OnValidate 共用,O(1) 重复检测。 private static System.Collections.Generic.Dictionary s_npcIdToPath; private static double s_npcIdsCacheTime = -10.0; private static System.Collections.Generic.Dictionary GetNpcIdCache() { double now = UnityEditor.EditorApplication.timeSinceStartup; if (s_npcIdToPath != null && now - s_npcIdsCacheTime < 5.0) return s_npcIdToPath; s_npcIdToPath = new System.Collections.Generic.Dictionary(System.StringComparer.Ordinal); string[] guids = UnityEditor.AssetDatabase.FindAssets("t:NpcSO"); foreach (var guid in guids) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); var npc = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (npc != null && !string.IsNullOrEmpty(npc.npcId) && !s_npcIdToPath.ContainsKey(npc.npcId)) s_npcIdToPath[npc.npcId] = path; } s_npcIdsCacheTime = now; return s_npcIdToPath; } private void OnValidate() { if (string.IsNullOrWhiteSpace(npcId)) { UnityEditor.EditorUtility.SetDirty(this); npcId = name; } var cache = GetNpcIdCache(); string myPath = UnityEditor.AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(myPath) && cache.TryGetValue(npcId, out var existingPath) && existingPath != myPath) { Debug.LogError( $"[NpcSO] npcId '{npcId}' 与 " + $"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this); s_npcIdsCacheTime = -10.0; } } #endif } }