feat(combat): 下劈弹跳完善——可破坏物可弹跳+独立弹跳力+下劈保留空中操控

- HitBox 新增 OnBreakableHitConfirmed:命中 IBreakable 也发命中确认,
  下劈打可破坏物同样弹跳;不走 OnHitConfirmed,灵力仍只来自敌人
- 新增 PogoBounceForce 配置(默认 15,略低于 JumpForce=18)+
  PlayerMovement.PogoBounce(),弹跳高度独立可调、固定不可截断
- DownAttackState 增加 OnStateFixedUpdate:下劈期间保留完整空中
  水平操控(含与 FallState 同款贴墙保护)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 09:51:17 +08:00
parent 3bafc4cbaa
commit 8515dfa7ab
6 changed files with 63 additions and 4 deletions

View File

@@ -29,6 +29,7 @@ MonoBehaviour:
DashCooldown: 0.4 DashCooldown: 0.4
DashInvincibilityDuration: 0.2 DashInvincibilityDuration: 0.2
DashInvincibilityCooldown: 0.9 DashInvincibilityCooldown: 0.9
PogoBounceForce: 15
DownDashSpeed: 22 DownDashSpeed: 22
DownDashDuration: 0.25 DownDashDuration: 0.25
WallSlideSpeed: 3 WallSlideSpeed: 3

View File

@@ -77,6 +77,13 @@ namespace BaseGames.Combat
/// <summary>命中确认委托PlayerCombat / EnemyCombat 订阅)。</summary> /// <summary>命中确认委托PlayerCombat / EnemyCombat 订阅)。</summary>
public event System.Action<DamageInfo> OnHitConfirmed; public event System.Action<DamageInfo> OnHitConfirmed;
/// <summary>
/// 命中可破坏物IBreakable确认事件。
/// 与 OnHitConfirmed 区分:不走 HurtBox 伤害流水线,不参与灵力获取;
/// 供下劈弹跳等"打到实体即生效"的逻辑订阅。
/// </summary>
public event System.Action<DamageInfo> OnBreakableHitConfirmed;
// 宿主投射物缓存Activate 时填入DamageInfo.SourceProjectile 写入用) // 宿主投射物缓存Activate 时填入DamageInfo.SourceProjectile 写入用)
private Projectile _ownerProjectile; private Projectile _ownerProjectile;
@@ -260,7 +267,10 @@ namespace BaseGames.Combat
// ③ 命中 IBreakable机关/障碍物) // ③ 命中 IBreakable机关/障碍物)
if (other.TryGetComponent<IBreakable>(out var breakable)) if (other.TryGetComponent<IBreakable>(out var breakable))
{
breakable.TryInteract(info); breakable.TryInteract(info);
OnBreakableHitConfirmed?.Invoke(info);
}
} }
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)──────────── // ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────

View File

@@ -214,6 +214,17 @@ namespace BaseGames.Player
_rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * _config.JumpCutMultiplier); _rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * _config.JumpCutMultiplier);
} }
/// <summary>
/// 下劈命中弹跳DownAttackState 调用)。覆盖当前垂直速度为 PogoBounceForce
/// 固定高度JumpCancelledEvent 仅 JumpState 处理,弹跳不受变高跳截断影响)。
/// </summary>
public void PogoBounce()
{
_rb.velocity = new Vector2(_rb.velocity.x, _config.PogoBounceForce);
_coyoteTimer = 0f;
_slopeSnapDisabled = true;
}
/// <summary> /// <summary>
/// 二段跳。覆盖当前垂直速度为 DoubleJumpForce。 /// 二段跳。覆盖当前垂直速度为 DoubleJumpForce。
/// FallState / JumpState 在检测到 HasAbility(DoubleJump) && AirJumpsLeft > 0 时调用。 /// FallState / JumpState 在检测到 HasAbility(DoubleJump) && AirJumpsLeft > 0 时调用。

View File

@@ -60,6 +60,10 @@ namespace BaseGames.Player
[Tooltip("无敌的独立冷却。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")] [Tooltip("无敌的独立冷却。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")]
public float DashInvincibilityCooldown = 0.9f; public float DashInvincibilityCooldown = 0.9f;
[Header("下劈弹跳Pogo")]
[Tooltip("下劈命中后的向上弹跳初速度。推荐略低于 JumpForce如 JumpForce=18 时取 15\n弹跳略矮于满跳固定高度不受变高跳截断影响。")]
public float PogoBounceForce = 15f;
[Header("下冲刺")] [Header("下冲刺")]
[Tooltip("向下冲刺速度(单位/秒)。推荐 22快速向下穿透空间。")] [Tooltip("向下冲刺速度(单位/秒)。推荐 22快速向下穿透空间。")]
public float DownDashSpeed = 22f; public float DownDashSpeed = 22f;

View File

@@ -58,10 +58,10 @@ namespace BaseGames.Player.States
{ {
if (_hasHitEnemy) return; if (_hasHitEnemy) return;
_hasHitEnemy = true; _hasHitEnemy = true;
// Pogo 弹跳:命中敌人后向上弹起,同时重置空中能力(等同落地效果) // Pogo 弹跳:命中后向上弹起(独立弹跳力,略矮于满跳),同时重置空中能力(等同落地效果)
Owner.ResetAirJumps(); Owner.ResetAirJumps();
Owner.GetState<DashState>()?.ResetDashCharge(); Owner.GetState<DashState>()?.ResetDashCharge();
Move.Jump(); Move.PogoBounce();
} }
public override void OnStateUpdate() public override void OnStateUpdate()
@@ -73,6 +73,25 @@ namespace BaseGames.Player.States
} }
} }
public override void OnStateFixedUpdate()
{
// 下劈期间保留完整空中水平操控(与 FallState 同款贴墙保护,防止压墙摩擦/卡角)
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
{
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
var wd = Owner.WallDetector;
bool currentFrameWall = inputDir > 0 ? Move.IsWallRight : Move.IsWallLeft;
if (currentFrameWall
|| (wd != null && (wd.IsTouchingWall && wd.WallDirection == inputDir
|| wd.HasPartialContact(inputDir))))
Move.ZeroHorizontalVelocity();
else
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
else
Move.ZeroHorizontalVelocity();
}
private void OnClipEnd() private void OnClipEnd()
{ {
if (_exited) return; if (_exited) return;

View File

@@ -31,7 +31,7 @@ namespace BaseGames.Player
private AttackDirection _activeDir; private AttackDirection _activeDir;
private IFeedbackPlayer _feedback; private IFeedbackPlayer _feedback;
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary> /// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。命中 HurtBox 或可破坏物均触发。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed; public event System.Action<DamageInfo> OnDownHitConfirmed;
/// <summary>任意 HitBox 命中确认事件(供 PlayerCombat 订阅通用命中反馈)。</summary> /// <summary>任意 HitBox 命中确认事件(供 PlayerCombat 订阅通用命中反馈)。</summary>
@@ -41,7 +41,10 @@ namespace BaseGames.Player
{ {
_allHitBoxes = GetComponentsInChildren<HitBox>(true); _allHitBoxes = GetComponentsInChildren<HitBox>(true);
foreach (var hb in _allHitBoxes) foreach (var hb in _allHitBoxes)
{
hb.OnHitConfirmed += OnAnyHitConfirmed; hb.OnHitConfirmed += OnAnyHitConfirmed;
hb.OnBreakableHitConfirmed += OnAnyBreakableHitConfirmed;
}
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance; _feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
} }
@@ -57,6 +60,17 @@ namespace BaseGames.Player
OnDownHitConfirmed?.Invoke(info); OnDownHitConfirmed?.Invoke(info);
} }
/// <summary>
/// 命中可破坏物:播放轻量打击反馈;下劈方向时同样触发弹跳。
/// 不转发 OnHitConfirmed可破坏物不参与灵力获取
/// </summary>
private void OnAnyBreakableHitConfirmed(DamageInfo info)
{
_feedback.PlayHit(HitWeight.Light);
if (_activeDir == AttackDirection.Down)
OnDownHitConfirmed?.Invoke(info);
}
// ── 公共 API ────────────────────────────────────────────────────────── // ── 公共 API ──────────────────────────────────────────────────────────
/// <summary> /// <summary>