using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using BaseGames.Quest; namespace BaseGames.Editor.Modules { /// /// DataHub 任务模块 —— 管理 QuestSO 资产。 /// public class QuestModule : IDataModule { private const string Folder = "Assets/_Game/Data/Quest"; private const string Prefix = "Quest_"; public string ModuleId => "quest"; public string DisplayName => "任务"; public string IconName => "d_UnityEditor.InspectorWindow"; private SoListPane _listPane; private DetailHeader _header; private QuestSO _selected; public void Initialize() { _listPane = new SoListPane( Folder, Prefix, s => { bool hasPre = s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0; return hasPre ? "有前置" : null; }); } public void BuildListPane(VisualElement container, Action onSelected) { _listPane.SelectionChanged = sel => { _selected = sel; onSelected?.Invoke(sel); }; container.Add(_listPane); _listPane.Refresh(); } public void BuildDetailPane(VisualElement container, UnityEngine.Object selected) { _selected = selected as QuestSO; _header = new DetailHeader(); _header.SetAsset(_selected); _header.RenameRequested += OnRenameRequested; container.Add(_header); if (_selected == null) return; container.Add(BuildInfoCard(_selected)); container.Add(BuildObjectivesList(_selected)); if (_selected.branches != null && _selected.branches.Length > 0) container.Add(BuildBranchesCard(_selected)); container.Add(BuildActionBar(_selected)); container.Add(SkillModule.MakeDivider()); container.Add(new InspectorElement(_selected)); } public void OnActivated() => _listPane?.Refresh(); // ── 内部 ───────────────────────────────────────────────────────────── private void OnRenameRequested(string newName) { if (_selected == null) return; var (ok, err) = AssetOperations.Rename(_selected, newName); if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定"); else { _header.SetAsset(_selected); _listPane.Invalidate(); } } private static VisualElement BuildInfoCard(QuestSO s) { var card = SkillModule.MakeCard(); int objCount = s.objectives != null ? s.objectives.Length : 0; SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(s.questId) ? "(未设置)" : s.questId); SkillModule.AddChip(card, "名称 Key", string.IsNullOrEmpty(s.displayNameKey) ? "(未设置)" : s.displayNameKey); if (!string.IsNullOrEmpty(s.descriptionKey)) SkillModule.AddChip(card, "描述 Key", s.descriptionKey); SkillModule.AddChip(card, "目标数", objCount.ToString()); if (s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0) { // 显示每个前置任务的 questId,方便策划一眼看清依赖链 var preIds = new System.Text.StringBuilder(); foreach (var pre in s.prerequisiteQuests) { if (pre == null) continue; if (preIds.Length > 0) preIds.Append(", "); preIds.Append(string.IsNullOrEmpty(pre.questId) ? pre.name : pre.questId); } if (preIds.Length > 0) SkillModule.AddChip(card, "前置任务", preIds.ToString()); } if (s.minAffinityToAccept > 0) SkillModule.AddChip(card, "好感门槛", s.minAffinityToAccept.ToString()); if (s.canFail) SkillModule.AddChip(card, "可失败", "✓"); if (s.reward != null) SkillModule.AddChip(card, "奖励", s.reward.name); return card; } private static VisualElement BuildObjectivesList(QuestSO s) { var section = new VisualElement(); section.style.paddingLeft = 12; section.style.paddingRight = 12; section.style.paddingTop = 6; section.style.paddingBottom = 6; var title = new Label("目标列表"); title.style.fontSize = 11; title.style.opacity = 0.55f; title.style.marginBottom = 4; title.style.unityFontStyleAndWeight = FontStyle.Bold; section.Add(title); if (s.objectives == null || s.objectives.Length == 0) { var empty = new Label("(无目标)"); empty.style.opacity = 0.4f; empty.style.fontSize = 11; section.Add(empty); return section; } for (int i = 0; i < s.objectives.Length; i++) { var obj = s.objectives[i]; if (obj == null) continue; var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.marginBottom = 3; // 序号 var idx = new Label($"{i + 1}."); idx.style.fontSize = 11; idx.style.opacity = 0.5f; idx.style.marginRight = 4; idx.style.width = 16; idx.style.flexShrink = 0; row.Add(idx); // 类型徽章 string badge = obj.BadgeLabel; var badgeLbl = new Label(badge); badgeLbl.style.fontSize = 10; badgeLbl.style.opacity = 0.7f; badgeLbl.style.marginRight = 6; badgeLbl.style.flexShrink = 0; badgeLbl.style.unityFontStyleAndWeight = FontStyle.Bold; row.Add(badgeLbl); // ID string id = string.IsNullOrEmpty(obj.objectiveId) ? obj.name : obj.objectiveId; var idLbl = new Label(id); idLbl.style.fontSize = 11; idLbl.style.flexGrow = 1; row.Add(idLbl); // 可选标记 if (obj.IsOptional) { var opt = new Label("[可选]"); opt.style.fontSize = 10; opt.style.opacity = 0.5f; opt.style.marginLeft = 4; row.Add(opt); } section.Add(row); } return section; } private static VisualElement BuildBranchesCard(QuestSO s) { var card = SkillModule.MakeCard(); card.style.flexDirection = FlexDirection.Column; var title = new Label("完成后分支"); title.style.fontSize = 11; title.style.opacity = 0.55f; title.style.marginBottom = 4; title.style.unityFontStyleAndWeight = FontStyle.Bold; card.Add(title); foreach (var branch in s.branches) { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.marginBottom = 2; string condition = branch.conditionQuest != null ? branch.conditionQuest.questId : "(默认)"; string next = branch.nextQuest != null ? branch.nextQuest.name : "(无)"; SkillModule.AddChip(row, "条件", condition); SkillModule.AddChip(row, "后续任务", next); // 优先显示新 SO 引用;回退到旧字段(Obsolete) string seqName = branch.npcDialogueSequence != null ? branch.npcDialogueSequence.name #pragma warning disable CS0618 : branch.npcDialogueKey; #pragma warning restore CS0618 if (!string.IsNullOrEmpty(seqName)) SkillModule.AddChip(row, "对话序列", seqName); card.Add(row); } return card; } private VisualElement BuildActionBar(QuestSO s) { var bar = SkillModule.BuildStandardActionBar( s, Folder, Prefix, onCreated: c => _listPane.Refresh(c), onCloned: c => _listPane.Refresh(c), onDeleted: () => _listPane.Refresh(null)); // 任务模块额外:代码常量生成 new Button(GenerateQuestKeys) { text = "生成常量" }.AlsoAddTo(bar); return bar; } // ── QuestKeys.cs 常量生成器 ────────────────────────────────────────── private const string GeneratedFolder = "Assets/_Game/Scripts/Generated"; private const string QuestKeysPath = GeneratedFolder + "/QuestKeys.cs"; private static void GenerateQuestKeys() { // 收集所有 questId var questIds = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (var q in AssetOperations.FindAll()) if (!string.IsNullOrWhiteSpace(q.questId)) questIds.Add(q.questId.Trim()); // 收集所有 targetNpcId(来自 TalkToNPC 目标 SO) var npcIds = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (var obj in AssetOperations.FindAll()) if (!string.IsNullOrWhiteSpace(obj.targetNpcId)) npcIds.Add(obj.targetNpcId.Trim()); var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("// 由 QuestModule 工具自动生成,请勿手动编辑。"); sb.AppendLine("// 重新生成:DataHub → 任务 → 任意任务 → 生成常量"); sb.AppendLine("// "); sb.AppendLine("namespace BaseGames.Quest"); sb.AppendLine("{"); sb.AppendLine(" /// 任务 ID 常量(从 QuestSO 自动生成)。"); sb.AppendLine(" public static class QuestKeys"); sb.AppendLine(" {"); sb.AppendLine(" /// 任务唯一 ID 常量。"); sb.AppendLine(" public static class Quest"); sb.AppendLine(" {"); if (questIds.Count == 0) sb.AppendLine(" // (未发现任何 QuestSO)"); foreach (var id in questIds) sb.AppendLine($" public const string {ToIdentifier(id)} = \"{id}\";"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// TalkToNPC 目标中使用的 NPC ID 常量。"); sb.AppendLine(" public static class NpcId"); sb.AppendLine(" {"); if (npcIds.Count == 0) sb.AppendLine(" // (未发现任何 TalkToNPCObjective)"); foreach (var id in npcIds) sb.AppendLine($" public const string {ToIdentifier(id)} = \"{id}\";"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine("}"); if (!Directory.Exists(GeneratedFolder)) Directory.CreateDirectory(GeneratedFolder); File.WriteAllText(QuestKeysPath, sb.ToString(), Encoding.UTF8); AssetDatabase.Refresh(); Debug.Log($"[QuestModule] QuestKeys.cs 已生成:{questIds.Count} 个任务 ID,{npcIds.Count} 个 NPC ID。"); EditorUtility.DisplayDialog("生成成功", $"QuestKeys.cs 已写入 {QuestKeysPath}\n任务 ID: {questIds.Count} NPC ID: {npcIds.Count}", "确定"); } /// 将任意字符串转换为合法的 C# 标识符(PascalCase)。C# 保留关键字加 @ 前缀。 private static string ToIdentifier(string raw) { if (string.IsNullOrEmpty(raw)) return "_Empty"; var parts = raw.Split('_', '-', ' ', '.', '/'); var sb = new StringBuilder(); foreach (var part in parts) { if (part.Length == 0) continue; sb.Append(char.ToUpperInvariant(part[0])); if (part.Length > 1) sb.Append(part.Substring(1)); } string result = sb.ToString(); if (result.Length > 0 && char.IsDigit(result[0])) result = "_" + result; if (string.IsNullOrEmpty(result)) return "_Empty"; // C# 保留关键字加 @ 前缀,避免生成无法编译的代码 if (s_CSharpKeywords.Contains(result)) result = "@" + result; return result; } private static readonly HashSet s_CSharpKeywords = new HashSet( System.StringComparer.Ordinal) { "abstract","as","base","bool","break","byte","case","catch","char","checked", "class","const","continue","decimal","default","delegate","do","double","else", "enum","event","explicit","extern","false","finally","fixed","float","for", "foreach","goto","if","implicit","in","int","interface","internal","is","lock", "long","namespace","new","null","object","operator","out","override","params", "private","protected","public","readonly","ref","return","sbyte","sealed", "short","sizeof","stackalloc","static","string","struct","switch","this", "throw","true","try","typeof","uint","ulong","unchecked","unsafe","ushort", "using","virtual","void","volatile","while" }; } }