diff --git a/Assets/_Game/Scripts/Combat/HitBox.cs b/Assets/_Game/Scripts/Combat/HitBox.cs index 25616b4..5f0d62a 100644 --- a/Assets/_Game/Scripts/Combat/HitBox.cs +++ b/Assets/_Game/Scripts/Combat/HitBox.cs @@ -61,6 +61,7 @@ namespace BaseGames.Combat _currentSource = source ?? _defaultSource; _attackerTransform = attacker ?? transform; _isActive = true; + _collider.enabled = true; // 缓存宿主 Rigidbody2D(沿父层级向上查找) _ownerRigidbody = _attackerTransform.GetComponentInParent(); // 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标) @@ -70,7 +71,8 @@ namespace BaseGames.Combat public void Deactivate() { - _isActive = false; + _isActive = false; + _collider.enabled = false; _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } @@ -87,6 +89,8 @@ namespace BaseGames.Combat _collider = GetComponent(); if (!_collider.isTrigger) Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this); + // 初始状态关闭碰撞体,防止未激活时产生物理检测 + _collider.enabled = false; // 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找 _clashService = ServiceLocator.GetOrDefault(); // 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null) @@ -96,6 +100,7 @@ namespace BaseGames.Combat private void OnDisable() { _isActive = false; + if (_collider != null) _collider.enabled = false; _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } @@ -180,116 +185,122 @@ namespace BaseGames.Combat { var col = GetComponent(); if (col == null) return; - // HitBox:激活 = 鲜红,非激活 = 极淡红轮廓 - Gizmos.color = _isActive - ? new Color(1f, 0.15f, 0.15f, 1f) - : new Color(1f, 0.15f, 0.15f, 0.2f); - DrawCollider2DWire(col); + Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f); + Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f); + DrawCollider2DFilled(col, fill, outline); } - // ──────────────────────────────────────────────────── - // Gizmo 辅助(内联,不依赖外部工具类) - // ──────────────────────────────────────────────────── - /// - /// 根据 Collider2D 类型绘制正确的 wire 轮廓。 - /// 使用 统一应用 Transform 的移动/旋转/缩放。 - /// - public static void DrawCollider2DWire(Collider2D col) + // ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)────────────────────────── + + /// 根据 Collider2D 类型绘制带填充色和轮廓的 2D Gizmo(供 HurtBox 等复用)。 + public static void DrawCollider2DFilled(Collider2D col, Color fill, Color outline) { - var prev = Gizmos.matrix; - Gizmos.matrix = col.transform.localToWorldMatrix; + var prevMatrix = UnityEditor.Handles.matrix; + UnityEditor.Handles.matrix = col.transform.localToWorldMatrix; switch (col) { case BoxCollider2D box: - DrawWireRect2D(box.offset, box.size); + DrawFilledRect2D(box.offset, box.size, fill, outline); + break; + + case CircleCollider2D circle: + DrawFilledCircle2D(circle.offset, circle.radius, fill, outline); break; case CapsuleCollider2D caps: - DrawWireCapsule2D(caps.offset, caps.size, caps.direction); + DrawFilledCapsule2D(caps.offset, caps.size, caps.direction, fill, outline); break; case PolygonCollider2D poly: for (int p = 0; p < poly.pathCount; p++) - { - var pts = poly.GetPath(p); - for (int i = 0; i < pts.Length; i++) - Gizmos.DrawLine(pts[i], pts[(i + 1) % pts.Length]); - } - break; - - case CircleCollider2D circle: - DrawWireCircle2D(circle.offset, circle.radius); + DrawFilledPolygonPath2D(poly.GetPath(p), fill, outline); break; default: - // 其他类型回退到 bounds 近似(恢复矩阵后在世界空间绘制) - Gizmos.matrix = prev; - DrawWireRect2D(col.bounds.center, col.bounds.size); - return; + UnityEditor.Handles.matrix = Matrix4x4.identity; + DrawFilledRect2D(col.bounds.center, col.bounds.size, fill, outline); + break; } - Gizmos.matrix = prev; + UnityEditor.Handles.matrix = prevMatrix; } - /// 在当前 Gizmos 坐标系中绘制轴对齐矩形(2D 线框,兼容透视相机)。 - public static void DrawWireRect2D(Vector2 center, Vector2 size) + /// 向后兼容的线框接口(内部改调 DrawCollider2DFilled)。 + public static void DrawCollider2DWire(Collider2D col) + => DrawCollider2DFilled(col, new Color(1f, 1f, 1f, 0.05f), new Color(1f, 1f, 1f, 0.8f)); + + private static void DrawFilledRect2D(Vector2 center, Vector2 size, Color fill, Color outline) { float hx = size.x * 0.5f, hy = size.y * 0.5f; - Vector3 tl = new Vector3(center.x - hx, center.y + hy, 0f); - Vector3 tr = new Vector3(center.x + hx, center.y + hy, 0f); - Vector3 br = new Vector3(center.x + hx, center.y - hy, 0f); - Vector3 bl = new Vector3(center.x - hx, center.y - hy, 0f); - Gizmos.DrawLine(tl, tr); - Gizmos.DrawLine(tr, br); - Gizmos.DrawLine(br, bl); - Gizmos.DrawLine(bl, tl); - } - - /// 用线段近似绘制 2D 圆周(不使用 DrawWireSphere,兼容透视相机)。 - public static void DrawWireCircle2D(Vector3 center, float radius, int segs = 32) - { - float step = 360f / segs; - Vector3 prevPt = center + new Vector3(radius, 0f, 0f); - for (int i = 1; i <= segs; i++) + var verts = new Vector3[] { - float a = i * step * Mathf.Deg2Rad; - Vector3 nextPt = center + new Vector3(Mathf.Cos(a) * radius, Mathf.Sin(a) * radius, 0f); - Gizmos.DrawLine(prevPt, nextPt); - prevPt = nextPt; - } + new Vector3(center.x - hx, center.y + hy, 0f), + new Vector3(center.x + hx, center.y + hy, 0f), + new Vector3(center.x + hx, center.y - hy, 0f), + new Vector3(center.x - hx, center.y - hy, 0f), + }; + UnityEditor.Handles.DrawSolidRectangleWithOutline(verts, fill, outline); } - /// 绘制 2D 胶囊轮廓(在 col.transform 局部坐标系中)。 - private static void DrawWireCapsule2D(Vector2 offset, Vector2 size, CapsuleDirection2D dir) + private static void DrawFilledCircle2D(Vector2 center, float radius, Color fill, Color outline) { - bool vert = dir == CapsuleDirection2D.Vertical; - float radius = vert ? size.x * 0.5f : size.y * 0.5f; - float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius); - Vector2 axis = vert ? Vector2.up : Vector2.right; - Vector2 perp = vert ? Vector2.right : Vector2.up; - Vector2 capA = offset + axis * half; - Vector2 capB = offset - axis * half; - - Gizmos.DrawLine(capA + perp * radius, capB + perp * radius); - Gizmos.DrawLine(capA - perp * radius, capB - perp * radius); - DrawArc2D(capA, radius, vert ? 0f : -90f, 180f); - DrawArc2D(capB, radius, vert ? 180f : 90f, 180f); + Vector3 c = new Vector3(center.x, center.y, 0f); + UnityEditor.Handles.color = fill; + UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, radius); + UnityEditor.Handles.color = outline; + UnityEditor.Handles.DrawWireDisc(c, Vector3.back, radius); } - /// 用多段直线近似绘制 2D 圆弧。 - private static void DrawArc2D(Vector2 c, float r, float startDeg, float spanDeg, int segs = 20) + private static readonly System.Collections.Generic.List _capsuleVertsBuf + = new System.Collections.Generic.List(64); + + private static void DrawFilledCapsule2D(Vector2 offset, Vector2 size, + CapsuleDirection2D dir, Color fill, Color outline) { - float step = spanDeg / segs; - var prev = c + new Vector2(Mathf.Cos(startDeg * Mathf.Deg2Rad) * r, - Mathf.Sin(startDeg * Mathf.Deg2Rad) * r); - for (int i = 1; i <= segs; i++) + bool vert = dir == CapsuleDirection2D.Vertical; + float radius = vert ? size.x * 0.5f : size.y * 0.5f; + float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius); + Vector2 axis = vert ? Vector2.up : Vector2.right; + Vector2 capA = offset + axis * half; // 顶部(竖向)或右端(横向)圆心 + Vector2 capB = offset - axis * half; // 底部(竖向)或左端(横向)圆心 + + const int segs = 12; + _capsuleVertsBuf.Clear(); + + float baseA = vert ? 0f : -Mathf.PI * 0.5f; + for (int i = 0; i <= segs; i++) { - float a = (startDeg + step * i) * Mathf.Deg2Rad; - var next = c + new Vector2(Mathf.Cos(a) * r, Mathf.Sin(a) * r); - Gizmos.DrawLine(prev, next); - prev = next; + float a = baseA + (float)i / segs * Mathf.PI; + _capsuleVertsBuf.Add(new Vector3(capA.x + Mathf.Cos(a) * radius, + capA.y + Mathf.Sin(a) * radius, 0f)); } + + float baseB = vert ? Mathf.PI : Mathf.PI * 0.5f; + for (int i = 0; i <= segs; i++) + { + float a = baseB + (float)i / segs * Mathf.PI; + _capsuleVertsBuf.Add(new Vector3(capB.x + Mathf.Cos(a) * radius, + capB.y + Mathf.Sin(a) * radius, 0f)); + } + + var arr = _capsuleVertsBuf.ToArray(); + UnityEditor.Handles.color = fill; + UnityEditor.Handles.DrawAAConvexPolygon(arr); + UnityEditor.Handles.color = outline; + for (int i = 0; i < arr.Length; i++) + UnityEditor.Handles.DrawLine(arr[i], arr[(i + 1) % arr.Length]); + } + + private static void DrawFilledPolygonPath2D(Vector2[] path, Color fill, Color outline) + { + if (path == null || path.Length < 3) return; + var verts = System.Array.ConvertAll(path, p => new Vector3(p.x, p.y, 0f)); + UnityEditor.Handles.color = fill; + UnityEditor.Handles.DrawAAConvexPolygon(verts); // 凹多边形仅轮廓准确,填充近似 + UnityEditor.Handles.color = outline; + for (int i = 0; i < verts.Length; i++) + UnityEditor.Handles.DrawLine(verts[i], verts[(i + 1) % verts.Length]); } #endif } diff --git a/Assets/_Game/Scripts/Combat/HurtBox.cs b/Assets/_Game/Scripts/Combat/HurtBox.cs index 3c6ed87..d6e6b68 100644 --- a/Assets/_Game/Scripts/Combat/HurtBox.cs +++ b/Assets/_Game/Scripts/Combat/HurtBox.cs @@ -123,13 +123,23 @@ namespace BaseGames.Combat { var col = GetComponent(); if (col == null) return; - // HurtBox:正常激活 = 青色,无敌 = 黄色,非激活 = 极淡青 - Gizmos.color = !_isActive - ? new Color(0f, 0.85f, 1f, 0.15f) - : _isHurtBoxInvincible - ? new Color(1f, 1f, 0f, 1f ) - : new Color(0f, 0.85f, 1f, 1f ); - HitBox.DrawCollider2DWire(col); + Color fill, outline; + if (!_isActive) + { + fill = new Color(0f, 0.85f, 1f, 0.05f); + outline = new Color(0f, 0.85f, 1f, 0.20f); + } + else if (_isHurtBoxInvincible) + { + fill = new Color(1f, 1f, 0f, 0.25f); + outline = new Color(1f, 1f, 0f, 0.90f); + } + else + { + fill = new Color(0f, 0.85f, 1f, 0.20f); + outline = new Color(0f, 0.85f, 1f, 0.90f); + } + HitBox.DrawCollider2DFilled(col, fill, outline); } #endif } diff --git a/Assets/_Game/Scripts/Editor/Equipment/AbilityTypeDrawer.cs b/Assets/_Game/Scripts/Editor/Equipment/AbilityTypeDrawer.cs index 58ff0e5..6c964d4 100644 --- a/Assets/_Game/Scripts/Editor/Equipment/AbilityTypeDrawer.cs +++ b/Assets/_Game/Scripts/Editor/Equipment/AbilityTypeDrawer.cs @@ -13,7 +13,8 @@ namespace BaseGames.Editor /// 分组: /// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / DownDash /// 法术能力 — Spell1 / Spell2 / Spell3 - /// 形态能力 — SpiritForm / SpiritDash + /// 灵魄形态 — SpiritForm / SpiritDash + /// 三魂形态 — FormTianHun / FormDiHun / FormMingHun /// 战斗能力 — Parry / ChargeAttack / DownSlash /// 互动能力 — Interact / FastTravel /// 能力强化 — InvincibleDash @@ -46,6 +47,12 @@ namespace BaseGames.Editor (AbilityType.SpiritForm, "灵魄形态"), (AbilityType.SpiritDash, "灵魄冲刺"), }), + ("三魂形态", new[] + { + (AbilityType.FormTianHun, "天魂"), + (AbilityType.FormDiHun, "地魂"), + (AbilityType.FormMingHun, "命魂"), + }), ("战斗能力", new[] { (AbilityType.Parry, "弹反"), diff --git a/Assets/_Game/Scripts/Editor/Tools/GMToolWindow.cs b/Assets/_Game/Scripts/Editor/Tools/GMToolWindow.cs index 878d05f..8f8fa97 100644 --- a/Assets/_Game/Scripts/Editor/Tools/GMToolWindow.cs +++ b/Assets/_Game/Scripts/Editor/Tools/GMToolWindow.cs @@ -51,6 +51,7 @@ namespace BaseGames.Editor }), ("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }), ("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }), + ("三魂形态", new[] { AbilityType.FormTianHun, AbilityType.FormDiHun, AbilityType.FormMingHun }), ("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }), ("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }), ("能力强化", new[] { AbilityType.InvincibleDash }), @@ -58,12 +59,13 @@ namespace BaseGames.Editor private static readonly string[] AbilityFlagNames = { - "贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下劈", + "贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下冲刺", "法术槽 1", "法术槽 2", "法术槽 3", "灵魄形态", "灵魄冲刺", "弹反", "蓄力攻击", "下斩", "互动", "快速旅行", "无敌冲刺", + "天魂", "地魂", "命魂", }; // ── 样式(懒初始化)────────────────────────────────────────────────── @@ -469,7 +471,10 @@ namespace BaseGames.Editor AbilityType.DownSlash => "下斩", AbilityType.Interact => "互动", AbilityType.FastTravel => "快速旅行", - AbilityType.InvincibleDash => "无敌冲刺", + AbilityType.InvincibleDash => "无敌冲刺", + AbilityType.FormTianHun => "天魂", + AbilityType.FormDiHun => "地魂", + AbilityType.FormMingHun => "命魂", _ => flag.ToString(), }; diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index 1ccae56..b43fe33 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -301,13 +301,24 @@ namespace BaseGames.Enemies #if UNITY_EDITOR if (_statsSO == null) return; - // ── 侦测范围(淡橙圆,始终可见)──────────────────────────── - Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.18f); - HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange); + // ── 侦测范围(淡橙填充圆,始终可见)+ 攻击范围(淡红填充圆)──────── + { + var c = new Vector3(transform.position.x, transform.position.y, 0f); + var prevM = UnityEditor.Handles.matrix; + UnityEditor.Handles.matrix = Matrix4x4.identity; - // ── 攻击范围(淡红圆)─────────────────────────────────────── - Gizmos.color = new Color(1f, 0.15f, 0.15f, 0.22f); - HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange); + UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f); + UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange); + UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.55f); + UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange); + + UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.15f); + UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange); + UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.55f); + UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange); + + UnityEditor.Handles.matrix = prevM; + } // ── 运行时:LOS 连线 ──────────────────────────────────────── if (!Application.isPlaying || _playerTransform == null) return; @@ -319,11 +330,10 @@ namespace BaseGames.Enemies // 眼睛位置小圆点(金黄) Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f); - HitBox.DrawWireCircle2D(eyeWorld, 0.07f, 8); + Gizmos.DrawWireSphere(eyeWorld, 0.07f); if (inRange || _losResult) { - // 有 LOS → 橙色实线;在范围内但遮挡 → 灰色虚感(透明度低) Gizmos.color = _losResult ? new Color(1f, 0.5f, 0f, 0.85f) : new Color(0.6f, 0.6f, 0.6f, 0.25f); @@ -338,11 +348,23 @@ namespace BaseGames.Enemies if (_statsSO == null) return; // 选中时加亮范围圆 - Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.6f); - HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange); + { + var c = new Vector3(transform.position.x, transform.position.y, 0f); + var prevM = UnityEditor.Handles.matrix; + UnityEditor.Handles.matrix = Matrix4x4.identity; - Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f); - HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange); + UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f); + UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange); + UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.90f); + UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange); + + UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.25f); + UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange); + UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.90f); + UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange); + + UnityEditor.Handles.matrix = prevM; + } #endif } } diff --git a/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs b/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs index dddd811..4e3ba76 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs @@ -45,6 +45,7 @@ namespace BaseGames.Enemies public void PlayFootstep() { } public void TriggerPreset(string presetId) { } public void PlaySFXById(string sfxId) { } + public void PlayFormSwitch(int formIndex) { } // 敌人无形态切换,空实现 // ── EnemyBase 语义方法 ──────────────────────────────────────────── /// 受到伤害时调用(由 EnemyBase 触发)。 diff --git a/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs b/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs index 0ceef5c..8d38f6d 100644 --- a/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs +++ b/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs @@ -44,6 +44,10 @@ namespace BaseGames.Feedback /// 通过 sfxId 触发单次音效(不带任何摄像机/振动反馈)。 void PlaySFXById(string sfxId); + + // ── 形态切换 ────────────────────────────────────────────────────────────── + /// 切换到指定形态时播放对应反馈(音效 + 粒子 + 震屏等)。formIndex 对应 FormType 枚举值。 + void PlayFormSwitch(int formIndex); } /// 命中力度。 @@ -67,6 +71,7 @@ namespace BaseGames.Feedback public void PlayFootstep() { } public void TriggerPreset(string presetId) { } public void PlaySFXById(string sfxId) { } + public void PlayFormSwitch(int formIndex) { } } } diff --git a/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs b/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs index a4ebf54..4fbe520 100644 --- a/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs +++ b/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs @@ -33,6 +33,10 @@ namespace BaseGames.Feedback [Header("脚步声材质检测")] [SerializeField] private BaseGames.Audio.FootstepSoundPlayer _footstepSoundPlayer; + [Header("形态切换反馈")] + [Tooltip("索引与 FormType 枚举值对应:0=天魂, 1=地魂, 2=命魂")] + [SerializeField] private MMF_Player[] _onFormSwitch = new MMF_Player[3]; + [Header("命名预设(可选)")] [SerializeField] private NamedFeedback[] _namedPresets; @@ -90,6 +94,12 @@ namespace BaseGames.Feedback Debug.LogWarning($"[PlayerFeedback] 未找到 SFX 预设 '{sfxId}'。"); } + public void PlayFormSwitch(int formIndex) + { + if (_onFormSwitch != null && formIndex >= 0 && formIndex < _onFormSwitch.Length) + _onFormSwitch[formIndex]?.PlayFeedbacks(); + } + // ── 辅助 ───────────────────────────────────────────────────────────────── private static Dictionary BuildMap(NamedFeedback[] entries) { diff --git a/Assets/_Game/Scripts/Player/AbilityType.cs b/Assets/_Game/Scripts/Player/AbilityType.cs index 4458124..a908cc7 100644 --- a/Assets/_Game/Scripts/Player/AbilityType.cs +++ b/Assets/_Game/Scripts/Player/AbilityType.cs @@ -47,9 +47,15 @@ namespace BaseGames.Player /// InvincibleDash = 1u << 18, + // ── 形态解锁(三魂形态)────────────────────────────────────────────────── + FormTianHun = 1u << 19, // 天魂形态(默认初始解锁) + FormDiHun = 1u << 20, // 地魂形态(游戏进程中解锁) + FormMingHun = 1u << 21, // 命魂形态(游戏进程中解锁) + // ── 组合掩码 ───────────────────────────────────────────────────────── AllMovement = WallCling | WallJump | Dash | DoubleJump | SuperJump | Swim | DownDash | InvincibleDash, AllSpells = Spell1 | Spell2 | Spell3, AllSpirit = SpiritForm | SpiritDash, + AllForms = FormTianHun | FormDiHun | FormMingHun, } } diff --git a/Assets/_Game/Scripts/Player/FormConfigSO.cs b/Assets/_Game/Scripts/Player/FormConfigSO.cs index 429799b..efe63d4 100644 --- a/Assets/_Game/Scripts/Player/FormConfigSO.cs +++ b/Assets/_Game/Scripts/Player/FormConfigSO.cs @@ -13,6 +13,11 @@ namespace BaseGames.Player [Header("形态列表 (forms[0]=Sky, forms[1]=Earth, forms[2]=Death)")] public FormSO[] forms; + [Header("切换冷却")] + [Tooltip("形态切换冷却时长(秒)。CD 内重复按键不生效。推荐 0.5。")] + [Min(0f)] + public float SwitchCooldown = 0.5f; + /// 按形态类型查找对应 FormSO;找不到返回 null。 public FormSO GetFormByType(FormType type) { diff --git a/Assets/_Game/Scripts/Player/FormController.cs b/Assets/_Game/Scripts/Player/FormController.cs index fd1a55a..cf42657 100644 --- a/Assets/_Game/Scripts/Player/FormController.cs +++ b/Assets/_Game/Scripts/Player/FormController.cs @@ -3,6 +3,7 @@ using UnityEngine; using BaseGames.Core; using BaseGames.Core.Save; using BaseGames.Core.Events; +using BaseGames.Feedback; using BaseGames.Input; namespace BaseGames.Player @@ -21,6 +22,9 @@ namespace BaseGames.Player [SerializeField] private FormConfigSO _config; [SerializeField] private InputReaderSO _input; + [Header("依赖")] + [SerializeField] private PlayerStats _stats; + [Header("事件频道")] [SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引(UI/Save) [SerializeField] private VoidEventChannelSO _onSkillSetChanged; // 通知 SkillHUD 刷新 @@ -29,12 +33,21 @@ namespace BaseGames.Player public FormSO CurrentForm { get; private set; } public FormSO[] AllForms => _config.forms; + /// 当前切换 CD 剩余时间(秒),0 表示可切换。 + public float SwitchCooldownRemaining => _switchCooldownTimer; + /// C# 事件,WeaponManager 在 OnEnable 自订阅(架构 05 §6)。 public event Action OnFormChanged; + private float _switchCooldownTimer; + private IFeedbackPlayer _feedback; + private void Awake() { Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this); + if (_stats == null) + _stats = GetComponent(); + _feedback = GetComponentInChildren() ?? NullFeedbackPlayer.Instance; } private void OnEnable() @@ -61,16 +74,43 @@ namespace BaseGames.Player CurrentForm = _config.forms[0]; } + private void Update() + { + if (_switchCooldownTimer > 0f) + _switchCooldownTimer -= Time.deltaTime; + } + // ── 公共 API ──────────────────────────────────────────────────────────── - /// 切换到指定形态类型。若已在目标形态则不操作。 + /// 切换到指定形态类型。若已在目标形态、尚未解锁或 CD 未结束则不操作。 public void SwitchForm(FormType newFormType) { + // CD 检查 + if (_switchCooldownTimer > 0f) return; + + // 检查对应形态的解锁标志 + AbilityType required = newFormType switch + { + FormType.TianHun => AbilityType.FormTianHun, + FormType.DiHun => AbilityType.FormDiHun, + FormType.MingHun => AbilityType.FormMingHun, + _ => AbilityType.None, + }; + if (required != AbilityType.None && _stats != null && !_stats.HasAbility(required)) + return; + FormSO newForm = _config.GetFormByType(newFormType); if (newForm == null || newForm == CurrentForm) return; CurrentForm = newForm; + // 启动切换 CD + if (_config.SwitchCooldown > 0f) + _switchCooldownTimer = _config.SwitchCooldown; + + // 播放对应形态切换反馈 + _feedback.PlayFormSwitch((int)newFormType); + // 1. SO 事件广播索引(UI/Save) _onFormChanged?.Raise(_config.GetFormIndex(newForm)); diff --git a/Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs b/Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs index 6dbb7ee..9f5e74c 100644 --- a/Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs +++ b/Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs @@ -33,6 +33,10 @@ namespace BaseGames.Player [Header("弹簧")] public AnimationClip UseSpring; + [Header("下冲刺")] + [Tooltip("向下冲刺动画。留空则复用 Fall 动画。")] + public AnimationClip DownDash; + [Header("弹反")] public AnimationClip ParryStart; public AnimationClip ParrySuccess; diff --git a/Assets/_Game/Scripts/Player/PlayerCombat.cs b/Assets/_Game/Scripts/Player/PlayerCombat.cs index 4924203..da8ddb2 100644 --- a/Assets/_Game/Scripts/Player/PlayerCombat.cs +++ b/Assets/_Game/Scripts/Player/PlayerCombat.cs @@ -34,21 +34,25 @@ namespace BaseGames.Player { if (_weaponManager != null) _weaponManager.OnWeaponChanged -= HandleWeaponChanged; - UnsubscribeDownHit(); + UnsubscribeHitEvents(); } private void HandleWeaponChanged(WeaponSO _) { - UnsubscribeDownHit(); + UnsubscribeHitEvents(); _currentHitBoxInstance = _weaponManager.ActiveHitBoxInstance; if (_currentHitBoxInstance != null) + { _currentHitBoxInstance.OnDownHitConfirmed += HandleDownHitConfirmed; + _currentHitBoxInstance.OnHitConfirmed += OnHitConfirmed; + } } - private void UnsubscribeDownHit() + private void UnsubscribeHitEvents() { if (_currentHitBoxInstance == null) return; _currentHitBoxInstance.OnDownHitConfirmed -= HandleDownHitConfirmed; + _currentHitBoxInstance.OnHitConfirmed -= OnHitConfirmed; _currentHitBoxInstance = null; } diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs index 9cca125..8cc36d4 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovement.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs @@ -292,6 +292,15 @@ namespace BaseGames.Player _rb.velocity = new Vector2(direction.x * speed, 0f); } + /// + /// 施加向下冲刺速度(DownDashState 调用)。 + /// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。 + /// + public void DownDash(float speed) + { + _rb.velocity = new Vector2(0f, -speed); + } + /// /// 壁滑:将垂直速度限制为 -speed(每帧调用以约束最大下滑速度)。 /// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。 @@ -422,7 +431,15 @@ namespace BaseGames.Player Gizmos.color = grounded ? new Color(0.1f, 1f, 0.3f, 0.9f) : new Color(0.4f, 0.85f, 0.4f, 0.35f); - BaseGames.Combat.HitBox.DrawWireRect2D(_groundCheck.position, _groundCheckSize); + // 绘制地面检测矩形 + { + Vector3 c = _groundCheck.position; + float hx = _groundCheckSize.x * 0.5f, hy = _groundCheckSize.y * 0.5f; + Gizmos.DrawLine(new Vector3(c.x - hx, c.y + hy), new Vector3(c.x + hx, c.y + hy)); + Gizmos.DrawLine(new Vector3(c.x + hx, c.y + hy), new Vector3(c.x + hx, c.y - hy)); + Gizmos.DrawLine(new Vector3(c.x + hx, c.y - hy), new Vector3(c.x - hx, c.y - hy)); + Gizmos.DrawLine(new Vector3(c.x - hx, c.y - hy), new Vector3(c.x - hx, c.y + hy)); + } } // ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)───────── @@ -436,11 +453,11 @@ namespace BaseGames.Player Gizmos.color = lHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f); Gizmos.DrawRay(wallPos, Vector2.left * wLen); - BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.left * wLen), 0.04f, 8); + Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.left * wLen), 0.04f); Gizmos.color = rHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f); Gizmos.DrawRay(wallPos, Vector2.right * wLen); - BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.right * wLen), 0.04f, 8); + Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.right * wLen), 0.04f); #endif } diff --git a/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs b/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs index 4d882c6..e5e610c 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs @@ -54,6 +54,12 @@ namespace BaseGames.Player [Tooltip("无敌的独立冷却(秒)。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")] public float DashInvincibilityCooldown = 0.9f; + [Header("下冲刺")] + [Tooltip("向下冲刺速度(单位/秒)。推荐 22,快速向下穿透空间。")] + public float DownDashSpeed = 22f; + [Tooltip("向下冲刺持续时长(秒)。推荐 0.25s。")] + public float DownDashDuration = 0.25f; + [Header("抓墙 / 壁滑")] [Tooltip("受限抓墙时(高于 wallGrabY)的下滑速度(单位/秒)。推荐 2。")] public float WallSlideSpeed = 2f; diff --git a/Assets/_Game/Scripts/Player/PlayerStats.cs b/Assets/_Game/Scripts/Player/PlayerStats.cs index 1e9820a..f945fc2 100644 --- a/Assets/_Game/Scripts/Player/PlayerStats.cs +++ b/Assets/_Game/Scripts/Player/PlayerStats.cs @@ -257,12 +257,29 @@ namespace BaseGames.Player } // ── Spring ──────────────────────────────────────────────────────────── - public bool UseSpring() + + /// + /// 仅扣除灵泉充能(SpringState 进入前摇时调用)。 + /// 回血需前摇结束后调用 。 + /// + public bool ConsumeSpringCharge() { if (CurrentSpringCharges <= 0) return false; CurrentSpringCharges--; _onSpringChargesChanged?.Raise(CurrentSpringCharges); - HealHP(_config.SpringHealAmount); + return true; + } + + /// + /// 执行灵泉回血(SpringState 前摇动画结束时调用)。 + /// + public void ApplySpringHeal() => HealHP(_config.SpringHealAmount); + + /// 扣除充能并立即回血(兼容旧调用路径)。 + public bool UseSpring() + { + if (!ConsumeSpringCharge()) return false; + ApplySpringHeal(); return true; } diff --git a/Assets/_Game/Scripts/Player/PlayerStatsSO.cs b/Assets/_Game/Scripts/Player/PlayerStatsSO.cs index 8f5f080..2c2d60f 100644 --- a/Assets/_Game/Scripts/Player/PlayerStatsSO.cs +++ b/Assets/_Game/Scripts/Player/PlayerStatsSO.cs @@ -28,9 +28,10 @@ namespace BaseGames.Player [Header("初始已解锁能力")] [Tooltip("角色出生时默认持有的能力([Flags] 位掩码)。\n\n" + + "FormTianHun:天魂形态默认解锁,请务必勾选。\n\n" + "Dash:地面与空中冲刺统一控制,勾选后即可使用 DashState。\n\n" + "DoubleJump:追加次数由 PlayerMovementConfigSO.MaxAirJumps 决定。\n" + "落地或 Pogo 命中后次数自动重置。")] - public AbilityType InitialAbilities = AbilityType.None; + public AbilityType InitialAbilities = AbilityType.FormTianHun; } } diff --git a/Assets/_Game/Scripts/Player/States/AttackState.cs b/Assets/_Game/Scripts/Player/States/AttackState.cs index 9b36264..c832982 100644 --- a/Assets/_Game/Scripts/Player/States/AttackState.cs +++ b/Assets/_Game/Scripts/Player/States/AttackState.cs @@ -112,7 +112,10 @@ namespace BaseGames.Player.States var animState = Anim.Play(step.clip); animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速 + // 每次重播同一 ClipTransition 会复用同一 AnimancerState, + // 必须先清除旧事件再注册新事件,否则回调会累积叠加。 var events = animState.Events(this); + events.Clear(); events.OnEnd = OnClipEnd; // HitBox 时间窗口(capture step by value for closure safety) @@ -121,11 +124,24 @@ namespace BaseGames.Player.States Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground, capturedStep.hitBoxId, capturedStep.damageSource)); events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes()); + // 连击输入窗口 if (capturedStep.comboInputOpen > 0f) - events.Add(capturedStep.comboInputOpen, () => _comboWindowOpen = true); + { + events.Add(capturedStep.comboInputOpen, () => + { + _comboWindowOpen = true; + // 窗口刚开时,补检查 InputBuffer——玩家可能在窗口前就提前按键 + if (!_comboInputPending && Buffer.ConsumeAttack()) + _comboInputPending = true; + }); + } else - _comboWindowOpen = true; // 0 = 立即开放 + { + _comboWindowOpen = true; + if (!_comboInputPending && Buffer.ConsumeAttack()) + _comboInputPending = true; + } if (capturedStep.comboInputClose > 0f) events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false); @@ -141,11 +157,18 @@ namespace BaseGames.Player.States _comboWindowOpen = false; Move.SetCancelWindowOpen(false); - // 如果已有缓存输入,直接推进(零延迟连击) + // 有缓存连击输入且还不是最后一段 → 零延迟推进到下一段 if (_comboInputPending) { - AdvanceCombo(); - return; + _comboInputPending = false; + int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1; + if (_comboIndex < maxCombo - 1) + { + _comboIndex++; + PlayAttackClip(); + return; // 新动画已开始,不进入等待阶段 + } + // 已是最后一段:消耗掉多余输入,继续进入等待阶段(不 return) } // 进入动画后等待阶段 @@ -169,7 +192,7 @@ namespace BaseGames.Player.States } else { - // 已是最后一段,忽略多余输入,等待超时 + // 已是最后一段,忽略多余输入,等待超时回 Idle _comboInputPending = false; } } diff --git a/Assets/_Game/Scripts/Player/States/DashState.cs b/Assets/_Game/Scripts/Player/States/DashState.cs index 5107604..00cfe84 100644 --- a/Assets/_Game/Scripts/Player/States/DashState.cs +++ b/Assets/_Game/Scripts/Player/States/DashState.cs @@ -29,6 +29,9 @@ namespace BaseGames.Player.States /// 重置冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。 public void ResetDashCharge() => _dashChargeUsed = false; + /// 消耗空中冲刺次数(DownDashState 进入时调用,与普通空中冲刺共享次数上限)。 + public void ConsumeAirDashCharge() => _dashChargeUsed = true; + /// /// 无敌帧是否已冷却,即本次冲刺可以获得无敌。 /// diff --git a/Assets/_Game/Scripts/Player/States/DownDashState.cs b/Assets/_Game/Scripts/Player/States/DownDashState.cs new file mode 100644 index 0000000..7fa70bc --- /dev/null +++ b/Assets/_Game/Scripts/Player/States/DownDashState.cs @@ -0,0 +1,62 @@ +using UnityEngine; + +namespace BaseGames.Player.States +{ + /// + /// 向下冲刺状态(空中下 + 冲刺触发)。 + /// - 消耗空中冲刺次数(与普通冲刺共享,本次离地仅可用一次)。 + /// - 关闭重力,施加向下速度,持续 DownDashDuration 秒。 + /// - 提前着地或计时结束时退出。 + /// - 需解锁 AbilityType.DownDash 才能进入(FallState / JumpState 负责条件检查)。 + /// + public class DownDashState : PlayerStateBase + { + private float _timer; + + public DownDashState(PlayerController owner) : base(owner) { } + + public override void OnStateEnter() + { + // 消耗空中冲刺次数(与普通空中冲刺互斥) + Owner.GetState()?.ConsumeAirDashCharge(); + + _timer = Cfg.DownDashDuration; + + // 关闭重力,施加向下速度 + Move?.SetGravityScale(0f); + Move?.DownDash(Cfg.DownDashSpeed); + + // 优先播放专属动画,未配置时回退到 Fall 动画 + var clip = AnimCfg?.DownDash ?? AnimCfg?.Fall; + if (clip != null) Anim?.Play(clip); + } + + public override void OnStateUpdate() + { + _timer -= Time.deltaTime; + if (_timer <= 0f || Move.IsGrounded) + EndDownDash(); + } + + public override void OnStateFixedUpdate() + { + // 持续保持向下速度(防止摩擦力减速) + if (_timer > 0f && !Move.IsGrounded) + Move?.DownDash(Cfg.DownDashSpeed); + } + + public override void OnStateExit() + { + Move?.SetGravityScale(Cfg.DefaultGravityScale); + } + + private void EndDownDash() + { + Move?.ZeroVelocity(); + if (Move != null && Move.IsGrounded) + Owner.TransitionTo(Owner.GetState()); + else + Owner.TransitionTo(Owner.GetState()); + } + } +} diff --git a/Assets/_Game/Scripts/Player/States/DownDashState.cs.meta b/Assets/_Game/Scripts/Player/States/DownDashState.cs.meta new file mode 100644 index 0000000..374ddbc --- /dev/null +++ b/Assets/_Game/Scripts/Player/States/DownDashState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5c828df0b1d49d4bb45ced1da093e7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Player/States/FallState.cs b/Assets/_Game/Scripts/Player/States/FallState.cs index b97455f..41ed25d 100644 --- a/Assets/_Game/Scripts/Player/States/FallState.cs +++ b/Assets/_Game/Scripts/Player/States/FallState.cs @@ -6,6 +6,7 @@ namespace BaseGames.Player.States /// 下落状态。 /// - 郊狼跳:CoyoteTimer > 0 时按跳跃 → 一段跳(JumpState,使用 JumpForce)。 /// - 空中跳跃:CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState(使用 DoubleJumpForce)。 + /// - 下冲刺:HasAbility(DownDash) && 下方向 + 冲刺键 → DownDashState(优先于普通冲刺)。 /// - 冲刺:HasAbility(Dash) && DashState.CanDashMidAir → DashState(地面与空中统一,空中限一次)。 /// - 抓墙:贴墙时按下朝向墙壁的方向键 → WallSlideState。 /// - 增强下落重力(FallGravityMult)确保下落快于上升,手感紧实。 @@ -53,9 +54,20 @@ namespace BaseGames.Player.States return; } + // ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)────────────────────── + // 按住下方向 + 冲刺键,且已解锁 DownDash 能力、空中冲刺次数未耗尽 + var dashState = Owner.GetState(); + if (dashState != null && dashState.CanDashMidAir + && Input.MoveInput.y < -0.5f + && Stats != null && Stats.HasAbility(AbilityType.DownDash) + && Buffer.ConsumeDash()) + { + _owner.TransitionTo(_owner.GetState()); + return; + } + // ── 冲刺(地面/空中统一使用 DashState)──────────────────────────── // 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入 - var dashState = Owner.GetState(); if (dashState != null && dashState.CanDashMidAir && Stats != null && Stats.HasAbility(AbilityType.Dash) && Buffer.ConsumeDash()) diff --git a/Assets/_Game/Scripts/Player/States/JumpState.cs b/Assets/_Game/Scripts/Player/States/JumpState.cs index 5f36c98..c1bb7e6 100644 --- a/Assets/_Game/Scripts/Player/States/JumpState.cs +++ b/Assets/_Game/Scripts/Player/States/JumpState.cs @@ -52,9 +52,20 @@ namespace BaseGames.Player.States return; } + // ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)────────────────────── + // 按住下方向 + 冲刺键,且已解锁 DownDash 能力、空中冲刺次数未耗尽 + var dashState = Owner.GetState(); + if (dashState != null && dashState.CanDashMidAir + && Input.MoveInput.y < -0.5f + && Stats != null && Stats.HasAbility(AbilityType.DownDash) + && Buffer.ConsumeDash()) + { + _owner.TransitionTo(_owner.GetState()); + return; + } + // 冲刺(地面/空中统一使用 DashState,空中限一次,优先于二段跳:冲刺可保存二段跳机会) // 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入 - var dashState = Owner.GetState(); if (dashState != null && dashState.CanDashMidAir && Stats != null && Stats.HasAbility(AbilityType.Dash) && Buffer.ConsumeDash()) diff --git a/Assets/_Game/Scripts/Player/States/PlayerController.cs b/Assets/_Game/Scripts/Player/States/PlayerController.cs index 810629f..e40bf4e 100644 --- a/Assets/_Game/Scripts/Player/States/PlayerController.cs +++ b/Assets/_Game/Scripts/Player/States/PlayerController.cs @@ -350,6 +350,7 @@ namespace BaseGames.Player.States _states[typeof(FallState)] = new FallState(this); _states[typeof(AttackState)] = new AttackState(this); _states[typeof(DashState)] = new DashState(this); + _states[typeof(DownDashState)] = new DownDashState(this); _states[typeof(WallSlideState)] = new WallSlideState(this); _states[typeof(WallJumpState)] = new WallJumpState(this); _states[typeof(AirAttackState)] = new AirAttackState(this); diff --git a/Assets/_Game/Scripts/Player/States/SpringState.cs b/Assets/_Game/Scripts/Player/States/SpringState.cs index 2cae8c5..9a27ae3 100644 --- a/Assets/_Game/Scripts/Player/States/SpringState.cs +++ b/Assets/_Game/Scripts/Player/States/SpringState.cs @@ -14,11 +14,11 @@ namespace BaseGames.Player.States public override void OnStateEnter() { - // 消耗灵泉充能并治疗(PlayerStats.UseSpring 内部回复 HP) - bool used = Stats?.UseSpring() ?? false; + // 前摇开始时只扣除充能,不立即回血;回血在前摇结束后的 OnSpringEnd 中执行。 + // 若前摇被打断(受伤 → HurtState),OnStateExit 被调用,充能已扣除但 OnSpringEnd 不会执行,回血失败。 + bool used = Stats?.ConsumeSpringCharge() ?? false; if (!used) { - // 无充能时立即退出 Owner.TransitionTo(Owner.GetState()); return; } @@ -37,7 +37,7 @@ namespace BaseGames.Player.States } } - // 无动画则直接结束 + // 无动画配置则直接结束(视为前摇瞬间完成) OnSpringEnd(); } @@ -49,6 +49,8 @@ namespace BaseGames.Player.States private void OnSpringEnd() { + // 前摇正常结束 → 执行回血 + Stats?.ApplySpringHeal(); Owner.TransitionTo(Owner.GetState()); } } diff --git a/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs b/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs index 7d94ff5..4f62bb9 100644 --- a/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs +++ b/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs @@ -32,6 +32,9 @@ namespace BaseGames.Player /// 下劈命中确认事件(供 DownAttackState Pogo 逻辑)。 public event System.Action OnDownHitConfirmed; + /// 任意 HitBox 命中确认事件(供 PlayerCombat 订阅通用命中反馈)。 + public event System.Action OnHitConfirmed; + private void Awake() { _allHitBoxes = GetComponentsInChildren(true); @@ -41,6 +44,7 @@ namespace BaseGames.Player private void OnAnyHitConfirmed(DamageInfo info) { + OnHitConfirmed?.Invoke(info); if (_activeDir == AttackDirection.Down) OnDownHitConfirmed?.Invoke(info); } diff --git a/Assets/_Game/Scripts/Player/WeaponManager.cs b/Assets/_Game/Scripts/Player/WeaponManager.cs index 587e8f8..2e0f6c8 100644 --- a/Assets/_Game/Scripts/Player/WeaponManager.cs +++ b/Assets/_Game/Scripts/Player/WeaponManager.cs @@ -27,6 +27,9 @@ namespace BaseGames.Player // 护符注入的武器覆盖:Key = FormSO.formId,Value = 替换武器(架构 05 §7) private readonly Dictionary _overrides = new(); + // 对象池:避免每次切换形态时 Instantiate/Destroy(Key = WeaponSO,Value = 已创建实例) + private readonly Dictionary _hitBoxPool = new(); + private void Awake() { if (_formController != null && _formController.CurrentForm != null) @@ -70,22 +73,33 @@ namespace BaseGames.Player private void SetDirectWeapon(WeaponSO weapon) { - var oldInstance = ActiveHitBoxInstance; + // 归还旧实例到池(SetActive(false) 会触发 HitBox.OnDisable → Deactivate,自动关闭 Collider2D) + ActiveHitBoxInstance?.gameObject.SetActive(false); ActiveWeapon = weapon; ActiveHitBoxInstance = null; if (weapon?.hitBoxPrefab != null && _weaponSocket != null) { - var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket); - ActiveHitBoxInstance = go.GetComponent(); + if (!_hitBoxPool.TryGetValue(weapon, out var pooled) || pooled == null) + { + var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket); + pooled = go.GetComponent(); + _hitBoxPool[weapon] = pooled; + } + pooled.gameObject.SetActive(true); + ActiveHitBoxInstance = pooled; } - // 先通知订阅者(使其有机会取消对旧实例的事件订阅),再销毁旧实例 + // 通知订阅者(PlayerCombat 取消旧实例事件订阅,订阅新实例) OnWeaponChanged?.Invoke(weapon); + } - if (oldInstance != null) - Destroy(oldInstance.gameObject); + private void OnDestroy() + { + foreach (var inst in _hitBoxPool.Values) + if (inst != null) Destroy(inst.gameObject); + _hitBoxPool.Clear(); } // ── 护符 Override API(由 WeaponOverrideEffect 调用,架构 05 §7)──────