- Implemented WorldStateFlagAttribute to mark string fields as world state flags. - Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states. - Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector. - Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors. - Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars. - Established QuestModule for managing QuestSO assets, including objectives and branches. - Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
343 lines
14 KiB
C#
343 lines
14 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// DataHub 任务模块 —— 管理 QuestSO 资产。
|
||
/// </summary>
|
||
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<QuestSO> _listPane;
|
||
private DetailHeader _header;
|
||
private QuestSO _selected;
|
||
|
||
public void Initialize()
|
||
{
|
||
_listPane = new SoListPane<QuestSO>(
|
||
Folder, Prefix,
|
||
s =>
|
||
{
|
||
bool hasPre = s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0;
|
||
return hasPre ? "有前置" : null;
|
||
});
|
||
}
|
||
|
||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> 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<string>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var q in AssetOperations.FindAll<QuestSO>())
|
||
if (!string.IsNullOrWhiteSpace(q.questId))
|
||
questIds.Add(q.questId.Trim());
|
||
|
||
// 收集所有 targetNpcId(来自 TalkToNPC 目标 SO)
|
||
var npcIds = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var obj in AssetOperations.FindAll<TalkToNPCObjective>())
|
||
if (!string.IsNullOrWhiteSpace(obj.targetNpcId))
|
||
npcIds.Add(obj.targetNpcId.Trim());
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("// <auto-generated>");
|
||
sb.AppendLine("// 由 QuestModule 工具自动生成,请勿手动编辑。");
|
||
sb.AppendLine("// 重新生成:DataHub → 任务 → 任意任务 → 生成常量");
|
||
sb.AppendLine("// </auto-generated>");
|
||
sb.AppendLine("namespace BaseGames.Quest");
|
||
sb.AppendLine("{");
|
||
sb.AppendLine(" /// <summary>任务 ID 常量(从 QuestSO 自动生成)。</summary>");
|
||
sb.AppendLine(" public static class QuestKeys");
|
||
sb.AppendLine(" {");
|
||
|
||
sb.AppendLine(" /// <summary>任务唯一 ID 常量。</summary>");
|
||
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(" /// <summary>TalkToNPC 目标中使用的 NPC ID 常量。</summary>");
|
||
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}",
|
||
"确定");
|
||
}
|
||
|
||
/// <summary>将任意字符串转换为合法的 C# 标识符(PascalCase)。C# 保留关键字加 @ 前缀。</summary>
|
||
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<string> s_CSharpKeywords = new HashSet<string>(
|
||
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"
|
||
};
|
||
}
|
||
}
|