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>
This commit is contained in:
161
Assets/_Game/Scripts/Editor/Shared/AssetCreationWizard.cs
Normal file
161
Assets/_Game/Scripts/Editor/Shared/AssetCreationWizard.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace BaseGames.Editor.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产快速创建向导 —— 弹出式 EditorWindow,引导输入 ID 并预览文件名,
|
||||
/// 一键创建 ScriptableObject 到指定文件夹。
|
||||
/// 用法: AssetCreationWizard.Show<QuestSO>(folder, prefix, (asset, id) => { asset.questId = id; });
|
||||
/// </summary>
|
||||
public class AssetCreationWizard : EditorWindow
|
||||
{
|
||||
private string _folder;
|
||||
private string _prefix;
|
||||
private string _idInput = "";
|
||||
private string _typeName;
|
||||
private Type _assetType;
|
||||
private Action<ScriptableObject, string> _onCreated;
|
||||
|
||||
private TextField _idField;
|
||||
private Label _previewLabel;
|
||||
|
||||
// ── 公开入口 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 打开向导。资产创建完成后以 (asset, id) 形式回调,调用方可在回调中自行设置 ID 字段。
|
||||
/// </summary>
|
||||
public static void Show<T>(string folder, string prefix, Action<T, string> onCreated)
|
||||
where T : ScriptableObject
|
||||
{
|
||||
string displayName = typeof(T).Name.EndsWith("SO")
|
||||
? typeof(T).Name[..^2]
|
||||
: typeof(T).Name;
|
||||
|
||||
var win = CreateInstance<AssetCreationWizard>();
|
||||
win.titleContent = new GUIContent($"新建 {displayName}");
|
||||
win._folder = folder;
|
||||
win._prefix = prefix;
|
||||
win._assetType = typeof(T);
|
||||
win._typeName = displayName;
|
||||
win._onCreated = (so, id) => onCreated((T)so, id);
|
||||
win.minSize = new Vector2(320, 132);
|
||||
win.maxSize = new Vector2(480, 132);
|
||||
win.ShowUtility();
|
||||
}
|
||||
|
||||
// ── UI 构建 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
var root = rootVisualElement;
|
||||
root.style.paddingTop = 12;
|
||||
root.style.paddingBottom = 12;
|
||||
root.style.paddingLeft = 16;
|
||||
root.style.paddingRight = 16;
|
||||
|
||||
// 说明行
|
||||
var desc = new Label($"将在 {_folder} 中新建一个 {_typeName}");
|
||||
desc.style.fontSize = 10;
|
||||
desc.style.opacity = 0.55f;
|
||||
desc.style.marginBottom = 10;
|
||||
root.Add(desc);
|
||||
|
||||
// ID 输入行
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.marginBottom = 6;
|
||||
|
||||
var idLabel = new Label("ID:");
|
||||
idLabel.style.width = 36;
|
||||
idLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
idLabel.style.flexShrink = 0;
|
||||
row.Add(idLabel);
|
||||
|
||||
_idField = new TextField { value = "" };
|
||||
_idField.style.flexGrow = 1;
|
||||
_idField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
_idInput = evt.newValue;
|
||||
RefreshPreview();
|
||||
});
|
||||
row.Add(_idField);
|
||||
root.Add(row);
|
||||
|
||||
// 文件名预览
|
||||
_previewLabel = new Label("文件名:(请输入 ID)");
|
||||
_previewLabel.style.fontSize = 10;
|
||||
_previewLabel.style.opacity = 0.5f;
|
||||
_previewLabel.style.marginBottom = 12;
|
||||
root.Add(_previewLabel);
|
||||
|
||||
// 按钮行
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.style.flexDirection = FlexDirection.Row;
|
||||
btnRow.style.justifyContent = Justify.FlexEnd;
|
||||
|
||||
var cancelBtn = new Button(Close) { text = "取消" };
|
||||
cancelBtn.style.width = 56;
|
||||
cancelBtn.style.marginRight = 6;
|
||||
btnRow.Add(cancelBtn);
|
||||
|
||||
var createBtn = new Button(DoCreate) { text = "创建" };
|
||||
createBtn.style.width = 56;
|
||||
btnRow.Add(createBtn);
|
||||
|
||||
root.Add(btnRow);
|
||||
|
||||
// 自动聚焦 ID 输入框
|
||||
_idField.schedule.Execute(() => _idField.Focus()).StartingIn(50);
|
||||
}
|
||||
|
||||
// ── 私有逻辑 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
if (_previewLabel == null) return;
|
||||
_previewLabel.text = string.IsNullOrWhiteSpace(_idInput)
|
||||
? "文件名:(请输入 ID)"
|
||||
: $"文件名:{_prefix}{_idInput}.asset";
|
||||
}
|
||||
|
||||
private void DoCreate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_idInput))
|
||||
{
|
||||
EditorUtility.DisplayDialog("ID 不能为空", "请输入有效的 ID 后再创建。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Regex.IsMatch(_idInput, @"^[\w\-]+$"))
|
||||
{
|
||||
EditorUtility.DisplayDialog("ID 格式有误", "ID 只能包含字母、数字、下划线或连字符。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(_folder))
|
||||
Directory.CreateDirectory(_folder);
|
||||
|
||||
string path = $"{_folder}/{_prefix}{_idInput}.asset";
|
||||
if (File.Exists(path))
|
||||
{
|
||||
EditorUtility.DisplayDialog("文件已存在", $"路径已存在:\n{path}\n请更换 ID。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
var asset = CreateInstance(_assetType) as ScriptableObject;
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
Undo.RegisterCreatedObjectUndo(asset, $"创建 {_typeName} {_idInput}");
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
_onCreated?.Invoke(asset, _idInput);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,29 @@ namespace BaseGames.Editor
|
||||
// ── 事件(使用字段委托,允许外部直接赋值替换,避免累积)─────────────
|
||||
public Action<T> SelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 附加过滤条件(可选)。返回 true = 保留;返回 false = 过滤掉。
|
||||
/// 赋值后调用 <see cref="ApplyFilter()"/> 使其生效;置 null 时仅按文本搜索过滤。
|
||||
/// </summary>
|
||||
public Func<T, bool> ExtraFilter
|
||||
{
|
||||
get => _extraFilter;
|
||||
set { _extraFilter = value; ApplyFilter(); }
|
||||
}
|
||||
private Func<T, bool> _extraFilter;
|
||||
|
||||
/// <summary>
|
||||
/// 扩展搜索文本提供器(可选)。返回除资产名以外也纳入搜索的附加文本(如 ID、本地化 Key 等)。
|
||||
/// 搜索时将对 "资产名 + 返回值" 拼接文本做不区分大小写的包含匹配。
|
||||
/// 赋值后立即重新应用过滤。
|
||||
/// </summary>
|
||||
public Func<T, string> GetExtraSearchText
|
||||
{
|
||||
get => _getExtraSearchText;
|
||||
set { _getExtraSearchText = value; ApplyFilter(); }
|
||||
}
|
||||
private Func<T, string> _getExtraSearchText;
|
||||
|
||||
// ── 字段 ─────────────────────────────────────────────────────────────
|
||||
private readonly string _defaultFolder;
|
||||
private readonly string _defaultPrefix;
|
||||
@@ -205,9 +228,25 @@ namespace BaseGames.Editor
|
||||
_filtered.Clear();
|
||||
foreach (var item in _all)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_search) ||
|
||||
item.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
_filtered.Add(item);
|
||||
bool nameMatch;
|
||||
if (string.IsNullOrEmpty(_search))
|
||||
{
|
||||
nameMatch = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameMatch = item.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (!nameMatch && _getExtraSearchText != null)
|
||||
{
|
||||
var extra = _getExtraSearchText(item);
|
||||
if (!string.IsNullOrEmpty(extra))
|
||||
nameMatch = extra.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
if (!nameMatch) continue;
|
||||
if (_extraFilter != null && !_extraFilter(item)) continue;
|
||||
_filtered.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
_listView.RefreshItems();
|
||||
|
||||
Reference in New Issue
Block a user