feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management
- 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.
This commit is contained in:
230
Assets/_Game/Scripts/Editor/Modules/DialogueModule.cs
Normal file
230
Assets/_Game/Scripts/Editor/Modules/DialogueModule.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Dialogue;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub 对话序列模块 —— 管理 DialogueSequenceSO 资产。
|
||||
/// </summary>
|
||||
public class DialogueModule : IDataModule
|
||||
{
|
||||
private const string Folder = "Assets/_Game/Data/Dialogue";
|
||||
private const string Prefix = "DLG_";
|
||||
|
||||
public string ModuleId => "dialogue";
|
||||
public string DisplayName => "对话";
|
||||
public string IconName => "d_UnityEditor.ConsoleWindow";
|
||||
|
||||
private SoListPane<DialogueSequenceSO> _listPane;
|
||||
private DetailHeader _header;
|
||||
private DialogueSequenceSO _selected;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_listPane = new SoListPane<DialogueSequenceSO>(
|
||||
Folder, Prefix,
|
||||
s =>
|
||||
{
|
||||
int v = s.variants != null ? s.variants.Length : 0;
|
||||
return v > 0 ? $"{v}变体" : 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 DialogueSequenceSO;
|
||||
|
||||
_header = new DetailHeader();
|
||||
_header.SetAsset(_selected);
|
||||
_header.RenameRequested += OnRenameRequested;
|
||||
container.Add(_header);
|
||||
|
||||
if (_selected == null) return;
|
||||
|
||||
container.Add(BuildInfoCard(_selected));
|
||||
container.Add(BuildLinesPreview(_selected));
|
||||
if (_selected.variants != null && _selected.variants.Length > 0)
|
||||
container.Add(BuildVariantsCard(_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(DialogueSequenceSO s)
|
||||
{
|
||||
var card = SkillModule.MakeCard();
|
||||
int lineCount = s.lines != null ? s.lines.Length : 0;
|
||||
int variantCount = s.variants != null ? s.variants.Length : 0;
|
||||
|
||||
SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(s.sequenceId) ? "(未设置)" : s.sequenceId);
|
||||
SkillModule.AddChip(card, "行数", lineCount.ToString());
|
||||
if (variantCount > 0)
|
||||
SkillModule.AddChip(card, "变体数", variantCount.ToString());
|
||||
return card;
|
||||
}
|
||||
|
||||
private static VisualElement BuildLinesPreview(DialogueSequenceSO 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("对话预览(前 5 行)");
|
||||
title.style.fontSize = 11;
|
||||
title.style.opacity = 0.55f;
|
||||
title.style.marginBottom = 4;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
section.Add(title);
|
||||
|
||||
if (s.lines == null || s.lines.Length == 0)
|
||||
{
|
||||
var empty = new Label("(无对话行)");
|
||||
empty.style.opacity = 0.4f;
|
||||
empty.style.fontSize = 11;
|
||||
section.Add(empty);
|
||||
return section;
|
||||
}
|
||||
|
||||
int preview = Mathf.Min(5, s.lines.Length);
|
||||
for (int i = 0; i < preview; i++)
|
||||
{
|
||||
var line = s.lines[i];
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.marginBottom = 3;
|
||||
|
||||
// 头像图标(actor 优先,回退到直接字段)
|
||||
var portrait = line.ResolvedPortrait;
|
||||
if (portrait != null)
|
||||
{
|
||||
var img = new Image { image = portrait.texture };
|
||||
img.style.width = 18;
|
||||
img.style.height = 18;
|
||||
img.style.marginRight = 4;
|
||||
img.style.borderTopLeftRadius = 2;
|
||||
img.style.borderTopRightRadius = 2;
|
||||
img.style.borderBottomLeftRadius = 2;
|
||||
img.style.borderBottomRightRadius = 2;
|
||||
row.Add(img);
|
||||
}
|
||||
|
||||
// 语音图标
|
||||
if (line.voiceClip != null)
|
||||
{
|
||||
var ico = EditorGUIUtility.IconContent("d_AudioClip Icon");
|
||||
if (ico?.image != null)
|
||||
{
|
||||
var img = new Image { image = ico.image };
|
||||
img.style.width = 14;
|
||||
img.style.height = 14;
|
||||
img.style.marginRight = 4;
|
||||
row.Add(img);
|
||||
}
|
||||
}
|
||||
|
||||
// 说话人(actor 优先,回退到直接字段)
|
||||
string speakerKey = line.ResolvedNameKey;
|
||||
if (!string.IsNullOrEmpty(speakerKey))
|
||||
{
|
||||
var spk = new Label(speakerKey + ":");
|
||||
spk.style.fontSize = 11;
|
||||
spk.style.opacity = 0.55f;
|
||||
spk.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
spk.style.marginRight = 4;
|
||||
spk.style.flexShrink = 0;
|
||||
row.Add(spk);
|
||||
}
|
||||
|
||||
// 文本 key(尝试显示本地化实际内容,回退到 key 本身)
|
||||
string rawText = string.IsNullOrEmpty(line.textKey) ? "(空)" : line.textKey;
|
||||
string preview = string.IsNullOrEmpty(line.textKey)
|
||||
? "(空)"
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, "Dialogue") ?? rawText);
|
||||
if (preview.Length > 48) preview = preview[..48] + "…";
|
||||
var lbl = new Label(preview);
|
||||
lbl.style.fontSize = 11;
|
||||
lbl.style.overflow = Overflow.Hidden;
|
||||
row.Add(lbl);
|
||||
|
||||
section.Add(row);
|
||||
}
|
||||
|
||||
if (s.lines.Length > preview)
|
||||
{
|
||||
var more = new Label($"… 还有 {s.lines.Length - preview} 行");
|
||||
more.style.opacity = 0.4f;
|
||||
more.style.fontSize = 10;
|
||||
section.Add(more);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
private static VisualElement BuildVariantsCard(DialogueSequenceSO 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 v in s.variants)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.marginBottom = 2;
|
||||
|
||||
string flags = v.requiredFlags != null && v.requiredFlags.Length > 0
|
||||
? string.Join(", ", v.requiredFlags)
|
||||
: "(无条件)";
|
||||
SkillModule.AddChip(row, "条件", flags);
|
||||
SkillModule.AddChip(row, "替换序列", v.sequence != null ? v.sequence.name : "(未设置)");
|
||||
card.Add(row);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private VisualElement BuildActionBar(DialogueSequenceSO s)
|
||||
{
|
||||
return SkillModule.BuildStandardActionBar(
|
||||
s, Folder, Prefix,
|
||||
onCreated: c => _listPane.Refresh(c),
|
||||
onCloned: c => _listPane.Refresh(c),
|
||||
onDeleted: () => _listPane.Refresh(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user