Files
zeling_v2/Assets/_Game/Scripts/Editor/Modules/SkillModule.cs
Joywayer 6eaa83dc71 feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop
- QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip
- IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info)
- IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it
- IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event
- QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions
- DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix)
- DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks
- DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match
- WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API
- DialogueModule: List badge shows warning indicator for unconditional-shadowing variants
- DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 00:05:15 +08:00

250 lines
9.1 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;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Skills;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 技能模块 —— 管理 FormSkillSO 资产。
/// </summary>
public class SkillModule : IDataModule, IDataModuleOrdered
{
private const string Folder = "Assets/_Game/Data/Skills";
private const string Prefix = "SKL_";
public string ModuleId => "skill";
public string DisplayName => "技能";
public string IconName => null;
public int DisplayOrder => 20;
private SoListPane<FormSkillSO> _listPane;
private DetailHeader _header;
private FormSkillSO _selected;
public void Initialize()
{
_listPane = new SoListPane<FormSkillSO>(
Folder, Prefix,
s => s.resourceType.ToString());
}
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 FormSkillSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
// Stats Card
var card = BuildStatsCard(_selected);
container.Add(card);
// 操作按钮
container.Add(BuildActionBar(_selected));
container.Add(MakeDivider());
// Inspector
var insp = new InspectorElement(_selected); container.Add(insp);
}
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 BuildStatsCard(FormSkillSO s)
{
var card = MakeCard();
AddChip(card, "效果类型", s.effectType.ToString());
AddChip(card, "资源类型", s.resourceType.ToString());
AddChip(card, "冷却", $"{s.cooldown:F1}s");
AddChip(card, "消耗", s.baseCost.ToString());
if (!string.IsNullOrEmpty(s.skillId))
AddChip(card, "ID", s.skillId);
return card;
}
private VisualElement BuildActionBar(FormSkillSO s)
{
var bar = MakeActionBar();
new Button(() => { EditorGUIUtility.PingObject(s); Selection.activeObject = s; })
{ text = "定位" }.AlsoAddTo(bar);
new Button(() => { var c = AssetOperations.Clone(s, Folder); if (c != null) _listPane.Refresh(c); })
{ text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() => { if (AssetOperations.Delete(s)) _listPane.Refresh(null); }) { text = "删除" };
ApplyDeleteStyle(del);
del.AlsoAddTo(bar);
return bar;
}
// ── 共享构建辅助 ─────────────────────────────────────────────────────
internal static VisualElement MakeCard()
{
var c = new VisualElement();
c.style.flexDirection = FlexDirection.Row;
c.style.flexWrap = Wrap.Wrap;
c.style.paddingLeft = 12;
c.style.paddingRight = 12;
c.style.paddingTop = 8;
c.style.paddingBottom = 8;
c.style.marginBottom = 4;
c.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.08f));
c.style.borderBottomWidth = 1;
c.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return c;
}
internal static void AddChip(VisualElement parent, string label, string value)
{
var chip = new VisualElement();
chip.style.flexDirection = FlexDirection.Row;
chip.style.alignItems = Align.Center;
chip.style.marginRight = 14;
chip.style.marginBottom = 2;
var l = new Label(label + ":");
l.style.opacity = 0.6f;
l.style.fontSize = 11;
l.style.marginRight = 3;
chip.Add(l);
var v = new Label(value);
v.style.fontSize = 11;
v.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Bold;
chip.Add(v);
parent.Add(chip);
}
internal static VisualElement MakeActionBar()
{
var b = new VisualElement();
b.style.flexDirection = FlexDirection.Row;
b.style.paddingLeft = 12;
b.style.paddingRight = 12;
b.style.paddingTop = 6;
b.style.paddingBottom = 6;
b.style.flexWrap = Wrap.Wrap;
return b;
}
internal static VisualElement MakeDivider()
{
var d = new VisualElement();
d.style.height = 1;
d.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return d;
}
/// <summary>将删除按钮染成红色边框,统一各模块样式。</summary>
internal static void ApplyDeleteStyle(Button btn)
{
var red = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btn.style.borderLeftColor = red;
btn.style.borderRightColor = red;
btn.style.borderTopColor = red;
btn.style.borderBottomColor = red;
btn.style.borderLeftWidth = 1;
btn.style.borderRightWidth = 1;
btn.style.borderTopWidth = 1;
btn.style.borderBottomWidth = 1;
btn.style.marginLeft = 8;
}
/// <summary>
/// 为任意 ScriptableObject 模块生成标准 ActionBar新建 / 定位 / 克隆 / 删除)。
/// 各模块可在返回后向 bar 追加额外按钮。
/// </summary>
/// <param name="asset">当前选中资产。</param>
/// <param name="folder">资产所在文件夹(用于新建 / 克隆)。</param>
/// <param name="prefix">新建资产的文件名前缀。</param>
/// <param name="onCreated">新建完成回调(传入新资产)。</param>
/// <param name="onCloned">克隆完成回调(传入克隆资产)。</param>
/// <param name="onDeleted">删除完成回调。</param>
internal static VisualElement BuildStandardActionBar<T>(
T asset,
string folder,
string prefix,
Action<T> onCreated,
Action<T> onCloned,
Action onDeleted,
Action<Action<T>> wizardCreate = null) where T : UnityEngine.ScriptableObject
{
var bar = MakeActionBar();
new Button(() =>
{
if (wizardCreate != null)
{
wizardCreate(c => onCreated?.Invoke(c));
}
else
{
var c = AssetOperations.Create<T>(folder, prefix + "New");
if (c != null) onCreated?.Invoke(c);
}
}) { text = "新建" }.AlsoAddTo(bar);
new Button(() =>
{
EditorGUIUtility.PingObject(asset);
Selection.activeObject = asset;
}) { text = "定位" }.AlsoAddTo(bar);
new Button(() =>
{
var c = AssetOperations.Clone(asset, folder);
if (c != null) onCloned?.Invoke(c);
}) { text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() =>
{
if (AssetOperations.Delete(asset)) onDeleted?.Invoke();
}) { text = "删除" };
ApplyDeleteStyle(del);
del.AlsoAddTo(bar);
return bar;
}
}
// ── Button 扩展(模块内共用)─────────────────────────────────────────────
internal static class ButtonExtensions
{
public static Button AlsoAddTo(this Button btn, VisualElement parent)
{
parent.Add(btn);
return btn;
}
}
}