using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using Sirenix.Utilities; using SLSUtilities.General; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; using Sirenix.OdinInspector.Editor; using Sirenix.Utilities.Editor; #endif namespace Cielonos.MainGame.Map { [CreateAssetMenu(fileName = "ZoneData", menuName = "Cielonos/MainGame/Map/ZoneData", order = 1)] public partial class ZoneData : SerializedScriptableObject { [InlineProperty] public struct SpawnPointKey : IEquatable { [HorizontalGroup("Row")] [LabelText("Spawn Point")] [LabelWidth(90)] public string group; [HorizontalGroup("Row", Width = 50, MarginLeft = 10)] [HideLabel] public int index; public SpawnPointKey(string group, int index) { this.group = group; this.index = index; } public bool Equals(SpawnPointKey other) { return group == other.group && index == other.index; } public override bool Equals(object obj) { return obj is SpawnPointKey other && Equals(other); } public override int GetHashCode() { unchecked { return ((group != null ? group.GetHashCode() : 0) * 397) ^ index; } } public static bool operator ==(SpawnPointKey left, SpawnPointKey right) { return left.Equals(right); } public static bool operator !=(SpawnPointKey left, SpawnPointKey right) { return !left.Equals(right); } public override string ToString() => $"{group}_{index}"; } [System.Serializable] [InlineProperty] [HideReferenceObjectPicker] public class EnemySpawnMapping { [HideInInspector] [System.NonSerialized] public ZoneData parent; [FormerlySerializedAs("Group")] [HorizontalGroup("SpawnInfo", 0.25f), HideLabel] [ReadOnly] public string group; [FormerlySerializedAs("Index")] [HorizontalGroup("SpawnInfo", 0.15f), HideLabel] [ReadOnly] public int index; [FormerlySerializedAs("EnemyID")] [HorizontalGroup("SpawnInfo", 0.5f), HideLabel] [ValueDropdown("GetEnemyKeys")] [OnValueChanged("OnEnemyIDChanged")] public string enemyID; public EnemySpawnMapping() { } public EnemySpawnMapping(SpawnPointKey key, string val, ZoneData parent) { group = key.group; index = key.index; enemyID = val; this.parent = parent; } #if UNITY_EDITOR private void OnEnemyIDChanged() { if (parent != null) { parent.UpdateDictionaryEntry(group, index, enemyID); } } #endif private IEnumerable GetEnemyKeys() { return MapBaseCollection.Enemies.Keys; } } } public partial class ZoneData { #if UNITY_EDITOR private static void AddButtonOnTitleBarGUI(Action buttonAction, EditorIcon icon = null) { icon ??= EditorIcons.MagnifyingGlass; if (SirenixEditorGUI.ToolbarButton(icon)) { buttonAction?.Invoke(); } } #endif } public partial class ZoneData { [Title("Scene Info")] [ValueDropdown("GetAllSceneNames"), PropertyOrder(-1)] [InfoBox("当前场景和ZoneData的关联场景不匹配!", InfoMessageType.Error, VisibleIf = "@!IsCurrentSceneMatch")] public string sceneName; public bool IsCurrentSceneMatch => UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == sceneName; #if UNITY_EDITOR private List GetAllSceneNames() { List sceneNames = new List(); foreach (var scene in EditorBuildSettings.scenes) { if (scene.enabled) { string name = System.IO.Path.GetFileNameWithoutExtension(scene.path); sceneNames.Add(name); } } return sceneNames; } #endif } public partial class ZoneData { private void CollectSpawnPoints(List list, string groupName) { ZoneManager zoneManager = ZoneManager.Instance; zoneManager.RebuildMapData(); list.Clear(); if (zoneManager.spawnPoints.TryGetValue(groupName, out List points)) { for (int i = 0; i < points.Count; i++) { list.Add(new SpawnPointKey(groupName, i)); } } } } public partial class ZoneData { [Title("Spawn Settings")] [Title("Player", HorizontalLine = false, Bold = false)] [ListDrawerSettings(ShowIndexLabels = false, AddCopiesLastElement = true), PropertyOrder(10), LabelText("Player Spawns")] public List playerSpawns = new List() { new SpawnPointKey("Player", 0) }; } public partial class ZoneData { [SerializeField, HideInInspector] public Dictionary enemySpawns = new Dictionary(); #if UNITY_EDITOR [HideInInspector] private List selectedGroupsToSync = new List(); [Title("Enemy", HorizontalLine = false)] [ShowInInspector] [ListDrawerSettings( OnTitleBarGUI = "DrawEnemySpawnTitleBarGUI", ShowFoldout = true, CustomRemoveIndexFunction = "RemoveEnemySpawnsIndex", CustomRemoveElementFunction = "RemoveEnemySpawnsElement")] [LabelText("Enemy Spawns"), PropertyOrder(12)] private List EnemySpawnsInEditor { get => GetSortedMappings(); set { enemySpawns = new Dictionary(); foreach (EnemySpawnMapping item in value) { enemySpawns[new SpawnPointKey(item.group, item.index)] = item.enemyID; } EditorUtility.SetDirty(this); } } private void OnEnemySyncSettingsChanged() { ZoneManager zoneManager = ZoneManager.Instance; zoneManager.RebuildMapData(); Dictionary newDict = new Dictionary(); foreach (string group in selectedGroupsToSync) { if (zoneManager.spawnPoints.TryGetValue(group, out List points)) { for (int i = 0; i < points.Count; i++) { SpawnPointKey key = new SpawnPointKey(group, i); newDict[key] = enemySpawns != null ? enemySpawns.GetValueOrDefault(key, "") : ""; } } } enemySpawns = newDict; EditorUtility.SetDirty(this); } private List GetSortedMappings() { List list = new List(); if (enemySpawns == null) return list; list.AddRange(enemySpawns.Select(kvp => new EnemySpawnMapping(kvp.Key, kvp.Value, this))); list.Sort((a, b) => { int comp = string.Compare(a.group, b.group, System.StringComparison.Ordinal); if (comp != 0) return comp; return a.index.CompareTo(b.index); }); return list; } private void RemoveEnemySpawnsElement(EnemySpawnMapping element) { SpawnPointKey key = new SpawnPointKey(element.group, element.index); if (enemySpawns.Remove(key)) { EditorUtility.SetDirty(this); } } private void RemoveEnemySpawnsIndex(int index) { var list = GetSortedMappings(); if (index >= 0 && index < list.Count) { EnemySpawnMapping item = list[index]; SpawnPointKey key = new SpawnPointKey(item.group, item.index); if (enemySpawns.Remove(key)) { EditorUtility.SetDirty(this); } } } public void UpdateDictionaryEntry(string group, int index, string newEnemyID) { SpawnPointKey key = new SpawnPointKey(group, index); if (enemySpawns.ContainsKey(key)) { enemySpawns[key] = newEnemyID; EditorUtility.SetDirty(this); } } private void DrawEnemySpawnTitleBarGUI() => AddButtonOnTitleBarGUI(OpenEnemySyncSelector); private void OpenEnemySyncSelector() { ZoneManager zoneManager = ZoneManager.Instance; zoneManager.RebuildMapData(); GroupSyncPopup popup = new GroupSyncPopup(zoneManager.spawnPoints.Keys, selectedGroupsToSync, (result) => { selectedGroupsToSync = result; OnEnemySyncSettingsChanged(); }); OdinEditorWindow window = OdinEditorWindow.InspectObject(popup); float width = 400; float height = 200; var centerRect = GUIHelper.GetEditorWindowRect().AlignCenter(width, height); window.position = centerRect; window.ShowPopup(); } private class GroupSyncPopup { [LabelText("Available Groups")] [ListDrawerSettings(IsReadOnly = true, ShowIndexLabels = false, HideAddButton = true, HideRemoveButton = true)] [TableList(AlwaysExpanded = true, HideToolbar = true)] [ShowInInspector, PropertyOrder(1)] public List toggles = new List(); private readonly System.Action> onConfirm; public GroupSyncPopup(IEnumerable available, List current, System.Action> onConfirm) { this.onConfirm = onConfirm; foreach (string name in available) { toggles.Add(new GroupItem(name, current.Contains(name))); } } [Button, PropertyOrder(2)] public void Confirm() { List result = toggles.Where(t => t.sync).Select(t => t.name).ToList(); this.onConfirm?.Invoke(result); GUIHelper.CurrentWindow?.Close(); } public class GroupItem { [HideLabel, TableColumnWidth(30, Resizable = false)] public bool sync; [HideLabel, ReadOnly] public string name; public GroupItem(string name, bool sync) { this.name = name; this.sync = sync; } } } #endif } public partial class ZoneData { /// /// 交互物生成定义。 /// 在单个 ZoneData 关卡数据中,配置某种特定的交互物(例如 ExitGate, MechanicalTable)以及它在场景中对应的多个生成点。 /// [HideReferenceObjectPicker] public class InteractableSpawnDefinition { /// 交互物的唯一 ID(对应 MapBaseCollection.Interactables 中的 Key)。 [ValueDropdown("GetInteractableIDs")] [LabelText("Interactable")] [InlineButton("SyncFromGroup", SdfIconType.Search, "")] public string interactableId; /// 该交互物在场景里配置的生成点列表。 [LabelText("Spawn Points")] [ListDrawerSettings(ShowIndexLabels = false, CustomAddFunction = "AddSpawnPoint")] public List spawnPoints = new List(); #if UNITY_EDITOR /// /// 从当前编辑的场景中,自动搜索并同步该交互物 ID 对应的所有场景 SpawnPoint。 /// private void SyncFromGroup() { if (string.IsNullOrEmpty(interactableId)) return; ZoneManager zoneManager = ZoneManager.Instance; if (zoneManager == null) return; // 强制重建场景的生成点缓存 zoneManager.RebuildMapData(); spawnPoints.Clear(); // 查找场景中组名与 interactableId 相同的所有生成点 if (zoneManager.spawnPoints.TryGetValue(interactableId, out List points)) { for (int i = 0; i < points.Count; i++) { spawnPoints.Add(new SpawnPointKey(interactableId, i)); } } Debug.Log($"[ZoneData] 成功为交互物 '{interactableId}' 同步了 {spawnPoints.Count} 个生成点。"); // 标记脏数据以确保保存修改 EditorUtility.SetDirty(zoneManager); } /// 获取 MapBaseCollection 中注册的所有可用交互物 ID,供下拉菜单选择。 private IEnumerable GetInteractableIDs() => MapBaseCollection.Interactables.Keys; /// 添加新的生成点时的自定义初始方法。 private void AddSpawnPoint() { spawnPoints.Add(new ZoneData.SpawnPointKey(interactableId, 0)); } #endif } } public partial class ZoneData { /// /// 配置本关卡中所有特定位置的交互物及其生成点的统一列表。 /// 替换了先前臃肿且不易扩展的零散 List 字段。 /// [Title("Specified Locations", HorizontalLine = false)] [PropertyOrder(20)] [LabelText("Interactable Spawns")] [ListDrawerSettings(ListElementLabelName = "interactableId", CustomAddFunction = "AddInteractableSpawn")] public List interactableSpawns = new List(); #if UNITY_EDITOR /// 在 Odin 列表中点击添加交互物生成定义时的自定义创建方法。 private void AddInteractableSpawn() { interactableSpawns.Add(new InteractableSpawnDefinition()); } #endif } }