- 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>
162 lines
6.2 KiB
C#
162 lines
6.2 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|