Files
zeling_v2/Assets/_Game/Scripts/Dialogue/NpcSO.cs
2026-05-25 11:54:37 +08:00

106 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Dialogue
{
/// <summary>
/// NPC 元数据资产(架构 14_NarrativeModule §2
/// 将 NPC 的唯一 ID、本地化名称 Key、头像、好感度上限集中在一处管理。
///
/// 关联:
/// • <see cref="InteractableNPC"/> 通过 _npcId 字段与此 SO 对应。
/// • <see cref="DialogueActorSO"/> 管理对话 UI 侧头像/颜色(二者可共享同一 Sprite或独立维护
/// • <see cref="BaseGames.Quest.QuestSO"/> 的 <c>giverNpc</c> 字段直接引用此 SO避免手填字符串。
///
/// 资产路径Assets/_Game/Data/NPC/NPC_{npcId}.asset
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/NPC/NPC")]
public class NpcSO : ScriptableObject, ILocalizableAsset
{
[Header("标识")]
[Tooltip("NPC 唯一 ID如 \"NPC_Elder\"。需与 InteractableNPC._npcId 保持一致。")]
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<string, string> s_npcIdToPath;
private static double s_npcIdsCacheTime = -10.0;
private static System.Collections.Generic.Dictionary<string, string> 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<string, string>(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<NpcSO>(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.IsNullOrEmpty(localizationTable))
localizationTable = "UI";
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
public IEnumerable<LocalizationKeyRef> GetLocalizationKeys()
{
string table = string.IsNullOrEmpty(localizationTable) ? "UI" : localizationTable;
if (!string.IsNullOrEmpty(nameKey))
yield return new LocalizationKeyRef(nameKey, table, nameof(nameKey));
if (!string.IsNullOrEmpty(interactPromptKey))
yield return new LocalizationKeyRef(interactPromptKey, "UI", nameof(interactPromptKey));
}
}
}