using UnityEditor; using UnityEngine; using BaseGames.Player; using BaseGames.Player.States; namespace BaseGames.Editor { /// /// 开发阶段 GM 调试工具窗口(仅 Play Mode 有效)。 /// 功能:资源快速填充(灵铢/灵力/魄元)、能力解锁/锁定、形态切换、调试辅助。 /// 菜单:BaseGames / Tools / GM Debug Tool /// public class GMToolWindow : EditorWindow { // ── 菜单 ────────────────────────────────────────────────────────────── [MenuItem("BaseGames/Tools/GM Debug Tool", priority = 2)] public static void Open() { var wnd = GetWindow(); wnd.titleContent = new GUIContent("GM Debug Tool"); wnd.minSize = new Vector2(320, 500); } // ── 资源输入字段 ────────────────────────────────────────────────────── private int _lingZhuAmount = 9999; private int _soulPowerAmount = 100; private int _spiritAmount = 100; // ── 折叠状态 ────────────────────────────────────────────────────────── private bool _foldResources = true; private bool _foldJump = true; private bool _foldForms = true; private bool _foldAbilities = false; private bool _foldDebug = true; // ── 缓存(避免每帧 FindObjectOfType)───────────────────────────────── private PlayerStats _stats; private FormController _formCtrl; private PlayerController _playerCtrl; private double _lastCacheTime = -10; // ── 能力分组定义(与 AbilityTypeDrawer 保持一致)──────────────────── private static readonly (string label, AbilityType[] flags)[] AbilityGroups = { ("移动能力", new[] { AbilityType.WallCling, AbilityType.WallJump, AbilityType.Dash, AbilityType.Dash, AbilityType.DoubleJump, AbilityType.SuperJump, AbilityType.Swim, AbilityType.Dive, }), ("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }), ("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }), ("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }), ("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }), ("能力强化", new[] { AbilityType.InvincibleDash }), }; private static readonly string[] AbilityFlagNames = { "贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下劈", "法术槽 1", "法术槽 2", "法术槽 3", "灵魄形态", "灵魄冲刺", "弹反", "蓄力攻击", "下斩", "互动", "快速旅行", "无敌冲刺", }; // ── 样式(懒初始化)────────────────────────────────────────────────── private GUIStyle _headerStyle; private GUIStyle _boxStyle; // ── 滚动 ────────────────────────────────────────────────────────────── private Vector2 _scroll; // ── EditorWindow 回调 ───────────────────────────────────────────────── private void OnEnable() => EditorApplication.playModeStateChanged += OnPlayModeChanged; private void OnDisable() => EditorApplication.playModeStateChanged -= OnPlayModeChanged; private void OnPlayModeChanged(PlayModeStateChange state) { _stats = null; _formCtrl = null; _playerCtrl = null; Repaint(); } private void OnGUI() { EnsureStyles(); if (!Application.isPlaying) { EditorGUILayout.HelpBox("GM 工具仅在 Play Mode 下有效。\n请先运行游戏。", MessageType.Info); return; } RefreshCache(); if (_stats == null) { EditorGUILayout.HelpBox("场景中未找到 PlayerStats 组件。\n请确认玩家已生成。", MessageType.Warning); if (GUILayout.Button("重新扫描")) _lastCacheTime = -10; return; } _scroll = EditorGUILayout.BeginScrollView(_scroll); DrawResourceSection(); DrawJumpSection(); DrawFormSection(); DrawAbilitySection(); DrawDebugSection(); EditorGUILayout.EndScrollView(); } // ── 分区:资源 ──────────────────────────────────────────────────────── private void DrawResourceSection() { _foldResources = DrawFoldout(_foldResources, "资源快速填充"); if (!_foldResources) return; EditorGUILayout.BeginVertical(_boxStyle); // 灵铢 EditorGUILayout.LabelField("灵铢", EditorStyles.boldLabel); EditorGUILayout.LabelField($"当前:{_stats.CurrentLingZhu}", EditorStyles.miniLabel); EditorGUILayout.BeginHorizontal(); _lingZhuAmount = EditorGUILayout.IntField(_lingZhuAmount, GUILayout.Width(80)); if (GUILayout.Button("增加")) _stats.AddLingZhu(_lingZhuAmount); if (GUILayout.Button("设为 9999")) _stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); // 灵力(SoulPower) EditorGUILayout.LabelField("灵力(技能用)", EditorStyles.boldLabel); EditorGUILayout.LabelField($"当前:{_stats.CurrentSoulPower} / {_stats.MaxSoulPower}", EditorStyles.miniLabel); EditorGUILayout.BeginHorizontal(); _soulPowerAmount = EditorGUILayout.IntField(_soulPowerAmount, GUILayout.Width(80)); if (GUILayout.Button("增加")) _stats.AddSoulPower(_soulPowerAmount); if (GUILayout.Button("填满")) _stats.AddSoulPower(_stats.MaxSoulPower); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); // 魄元(SpiritPower) EditorGUILayout.LabelField("魄元(魄技能用)", EditorStyles.boldLabel); EditorGUILayout.LabelField($"当前:{_stats.CurrentSpiritPower} / {_stats.MaxSpiritPower}", EditorStyles.miniLabel); EditorGUILayout.BeginHorizontal(); _spiritAmount = EditorGUILayout.IntField(_spiritAmount, GUILayout.Width(80)); if (GUILayout.Button("增加")) _stats.AddSpiritPower(_spiritAmount); if (GUILayout.Button("填满")) _stats.AddSpiritPower(_stats.MaxSpiritPower); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); if (GUILayout.Button("▶ 全部资源填满")) { _stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu)); _stats.AddSoulPower(_stats.MaxSoulPower); _stats.AddSpiritPower(_stats.MaxSpiritPower); } EditorGUILayout.EndVertical(); } // ── 分区:跳跃快捷 ──────────────────────────────────────────────────── private void DrawJumpSection() { _foldJump = DrawFoldout(_foldJump, "跳跃能力快捷"); if (!_foldJump) return; EditorGUILayout.BeginVertical(_boxStyle); // ── 当前状态 ── bool hasDoubleJump = _stats.HasAbility(AbilityType.DoubleJump); bool hasDash = _stats.HasAbility(AbilityType.Dash); bool hasWallJump = _stats.HasAbility(AbilityType.WallJump); bool hasWallCling = _stats.HasAbility(AbilityType.WallCling); int airJumpsLeft = _playerCtrl != null ? _playerCtrl.AirJumpsLeft : -1; int maxAirJumps = _playerCtrl != null && _playerCtrl.MovConfig != null ? _playerCtrl.MovConfig.MaxAirJumps : -1; string airStr = airJumpsLeft >= 0 ? $"{airJumpsLeft} / {maxAirJumps}" : "N/A"; EditorGUILayout.LabelField( $"二段跳:{(hasDoubleJump ? "✔ 已解锁" : "✘ 未解锁")} | 腾空剩余:{airStr}", EditorStyles.miniLabel); // ── MaxAirJumps 控制 ── if (_playerCtrl != null && _playerCtrl.MovConfig != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("最大空中跳跃次数", GUILayout.Width(120)); int newMax = EditorGUILayout.IntSlider( _playerCtrl.MovConfig.MaxAirJumps, 1, 5, GUILayout.ExpandWidth(true)); if (newMax != _playerCtrl.MovConfig.MaxAirJumps) { _playerCtrl.MovConfig.MaxAirJumps = newMax; EditorUtility.SetDirty(_playerCtrl.MovConfig); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField( $" 1=二段跳 2=三段跳 3=四段跳…(需先解锁 DoubleJump 能力)", EditorStyles.miniLabel); } EditorGUILayout.Space(4); // ── 跳跃系列快捷按钮(每行 2 个)── EditorGUILayout.BeginHorizontal(); DrawToggleAbilityBtn(hasDoubleJump, AbilityType.DoubleJump, "二段跳"); DrawToggleAbilityBtn(hasDash, AbilityType.Dash, "冲刺(地面+空中)"); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); DrawToggleAbilityBtn(hasWallJump, AbilityType.WallJump, "墙跳"); DrawToggleAbilityBtn(hasWallCling, AbilityType.WallCling, "贴墙悬挂"); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); DrawToggleAbilityBtn(_stats.HasAbility(AbilityType.SuperJump), AbilityType.SuperJump, "超级跳"); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); // ── 批量快捷 ── EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("解锁全部移动能力")) { foreach (var f in new[] { AbilityType.Dash, AbilityType.DoubleJump, AbilityType.SuperJump, AbilityType.WallCling, AbilityType.WallJump, }) _stats.UnlockAbility(f); } if (GUILayout.Button("锁定全部移动能力")) { foreach (var f in new[] { AbilityType.Dash, AbilityType.DoubleJump, AbilityType.SuperJump, AbilityType.WallCling, AbilityType.WallJump, }) _stats.LockAbility(f); } EditorGUILayout.EndHorizontal(); EditorGUILayout.HelpBox( "MaxAirJumps 修改立即写入 ScriptableObject(持久化)。\n" + "AirJumpsLeft 在角色下次落地时按新值重置。", MessageType.None); EditorGUILayout.EndVertical(); } /// /// 绘制一个「已解锁 → 锁定 / 未解锁 → 解锁」的切换按钮。 /// private void DrawToggleAbilityBtn(bool hasIt, AbilityType flag, string label) { GUI.backgroundColor = hasIt ? new Color(0.6f, 1.0f, 0.6f) : new Color(1.0f, 0.85f, 0.6f); string btnText = hasIt ? $"✔ {label}" : $"✘ {label}"; if (GUILayout.Button(btnText)) { if (hasIt) _stats.LockAbility(flag); else _stats.UnlockAbility(flag); } GUI.backgroundColor = Color.white; } // ── 分区:形态 ──────────────────────────────────────────────────────── private void DrawFormSection() { _foldForms = DrawFoldout(_foldForms, "形态快速切换"); if (!_foldForms) return; EditorGUILayout.BeginVertical(_boxStyle); if (_formCtrl == null) { EditorGUILayout.HelpBox("场景中未找到 FormController 组件。", MessageType.Warning); } else { string cur = _formCtrl.CurrentForm != null ? _formCtrl.CurrentForm.displayName : "未知"; EditorGUILayout.LabelField($"当前形态:{cur}", EditorStyles.boldLabel); EditorGUILayout.Space(4); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("天魂")) SwitchForm(FormType.TianHun); if (GUILayout.Button("地魂")) SwitchForm(FormType.DiHun); if (GUILayout.Button("命魂")) SwitchForm(FormType.MingHun); EditorGUILayout.EndHorizontal(); EditorGUILayout.HelpBox("提示:切换形态前请确保已解锁灵魄形态(SpiritForm)能力。", MessageType.None); } EditorGUILayout.EndVertical(); } private void SwitchForm(FormType type) { // 确保 SpiritForm 能力已解锁(否则 FSM 可能拒绝形态切换) _stats.UnlockAbility(AbilityType.SpiritForm); _formCtrl.SwitchForm(type); } // ── 分区:能力 ──────────────────────────────────────────────────────── private void DrawAbilitySection() { _foldAbilities = DrawFoldout(_foldAbilities, "能力解锁 / 锁定"); if (!_foldAbilities) return; EditorGUILayout.BeginVertical(_boxStyle); // 快捷全选/全清 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("全部解锁")) { foreach (var (_, flags) in AbilityGroups) foreach (var f in flags) _stats.UnlockAbility(f); } if (GUILayout.Button("全部锁定")) { foreach (var (_, flags) in AbilityGroups) foreach (var f in flags) _stats.LockAbility(f); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); // 各分组 foreach (var (groupLabel, flags) in AbilityGroups) { EditorGUILayout.LabelField(groupLabel, EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); bool allOn = true; foreach (var f in flags) if (!_stats.HasAbility(f)) { allOn = false; break; } if (GUILayout.Button(allOn ? "全锁" : "全解", GUILayout.Width(42))) { foreach (var f in flags) { if (allOn) _stats.LockAbility(f); else _stats.UnlockAbility(f); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); int col = 0; foreach (var flag in flags) { bool has = _stats.HasAbility(flag); bool toggled = GUILayout.Toggle(has, FlagDisplayName(flag), GUILayout.Width(128)); if (toggled != has) { if (toggled) _stats.UnlockAbility(flag); else _stats.LockAbility(flag); } col++; if (col == 2) { col = 0; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); } EditorGUILayout.EndVertical(); } // ── 分区:调试辅助 ──────────────────────────────────────────────────── private void DrawDebugSection() { _foldDebug = DrawFoldout(_foldDebug, "调试辅助"); if (!_foldDebug) return; EditorGUILayout.BeginVertical(_boxStyle); // HP EditorGUILayout.LabelField($"HP:{_stats.CurrentHP} / {_stats.MaxHP}", EditorStyles.miniLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("满血")) _stats.FullHeal(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); // 弹簧充能 EditorGUILayout.LabelField($"弹力充能:{_stats.CurrentSpringCharges} / {_stats.MaxSpringCharges}", EditorStyles.miniLabel); if (GUILayout.Button("恢复全部弹力充能")) _stats.RestoreSpringCharges(); EditorGUILayout.Space(4); // 无敌模式(God Mode) bool godNow = _stats.IsInvincible; // 仅作参考,GodMode 内部字段 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("开启无敌模式")) _stats.SetGodMode(true); if (GUILayout.Button("关闭无敌模式")) _stats.SetGodMode(false); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); // 一键全开(开发快速进入测试状态) GUI.backgroundColor = new Color(0.7f, 1.0f, 0.7f); if (GUILayout.Button("▶ 一键满状态(资源 + 全能力 + 满血)", GUILayout.Height(32))) { _stats.FullHeal(); _stats.RestoreSpringCharges(); _stats.AddLingZhu(Mathf.Max(0, 9999 - _stats.CurrentLingZhu)); _stats.AddSoulPower(_stats.MaxSoulPower); _stats.AddSpiritPower(_stats.MaxSpiritPower); foreach (var (_, flags) in AbilityGroups) foreach (var f in flags) _stats.UnlockAbility(f); } GUI.backgroundColor = Color.white; EditorGUILayout.EndVertical(); } // ── 工具方法 ────────────────────────────────────────────────────────── private void RefreshCache() { if (EditorApplication.timeSinceStartup - _lastCacheTime < 2.0) return; _lastCacheTime = EditorApplication.timeSinceStartup; _stats = FindObjectOfType(); _formCtrl = FindObjectOfType(); _playerCtrl = FindObjectOfType(); } private bool DrawFoldout(bool state, string label) { EditorGUILayout.Space(4); bool next = EditorGUILayout.Foldout(state, label, true, _headerStyle); return next; } private static string FlagDisplayName(AbilityType flag) => flag switch { AbilityType.WallCling => "贴墙悬挂", AbilityType.WallJump => "墙跳", AbilityType.Dash => "冲刺", AbilityType.DoubleJump => "二段跳", AbilityType.SuperJump => "超级跳", AbilityType.Swim => "游泳", AbilityType.Dive => "下劈", AbilityType.Spell1 => "法术槽 1", AbilityType.Spell2 => "法术槽 2", AbilityType.Spell3 => "法术槽 3", AbilityType.SpiritForm => "灵魄形态", AbilityType.SpiritDash => "灵魄冲刺", AbilityType.Parry => "弹反", AbilityType.ChargeAttack => "蓄力攻击", AbilityType.DownSlash => "下斩", AbilityType.Interact => "互动", AbilityType.FastTravel => "快速旅行", AbilityType.InvincibleDash => "无敌冲刺", _ => flag.ToString(), }; private void EnsureStyles() { if (_headerStyle != null) return; _headerStyle = new GUIStyle(EditorStyles.foldout) { fontStyle = FontStyle.Bold, fontSize = 12, }; _boxStyle = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(8, 8, 6, 6), }; } // ── 自动刷新(每秒重绘以显示最新数值)────────────────────────────── private void OnInspectorUpdate() => Repaint(); } }