diff --git a/Assets/_Game/Scripts/Combat/CombatEnums.cs b/Assets/_Game/Scripts/Combat/CombatEnums.cs index c94b0e3..ea2cda0 100644 --- a/Assets/_Game/Scripts/Combat/CombatEnums.cs +++ b/Assets/_Game/Scripts/Combat/CombatEnums.cs @@ -33,6 +33,12 @@ namespace BaseGames.Combat NoKnockback = 1 << 7, /// 击飞:使敌人进入 KnockUp 状态(腾空 + 落地)。仅在伤害量 >= HitTierConfig.launchThreshold 时生效。 Launch = 1 << 8, + /// + /// 持续伤害(DoT)。承伤方据此区分"持续伤害"与"一次性打击": + /// 只扣血,不触发受击硬直/打断,也不授予新的无敌帧。 + /// 通常与 同时设置(DoT 既不被无敌挡、也不刷新无敌)。 + /// + IsDoT = 1 << 9, } // ── 交互标签 ──────────────────────────────────────────────────────────── diff --git a/Assets/_Game/Scripts/Combat/StatusEffects/FireEffect.cs b/Assets/_Game/Scripts/Combat/StatusEffects/FireEffect.cs index 8fd4381..875ee4f 100644 --- a/Assets/_Game/Scripts/Combat/StatusEffects/FireEffect.cs +++ b/Assets/_Game/Scripts/Combat/StatusEffects/FireEffect.cs @@ -32,7 +32,7 @@ namespace BaseGames.Combat.StatusEffects var info = new DamageInfo.Builder() .SetRaw(1) .SetType(DamageType.True) - .SetFlags(DamageFlags.IgnoreIFrame) + .SetFlags(DamageFlags.IgnoreIFrame | DamageFlags.IsDoT) .Build(); Owner?.ApplyDirectDamage(info); } diff --git a/Assets/_Game/Scripts/Combat/StatusEffects/PoisonEffect.cs b/Assets/_Game/Scripts/Combat/StatusEffects/PoisonEffect.cs index 186e972..ffc0b1e 100644 --- a/Assets/_Game/Scripts/Combat/StatusEffects/PoisonEffect.cs +++ b/Assets/_Game/Scripts/Combat/StatusEffects/PoisonEffect.cs @@ -34,7 +34,7 @@ namespace BaseGames.Combat.StatusEffects var info = new DamageInfo.Builder() .SetRaw(StackCount) // 叠层越多伤害越高 .SetType(DamageType.True) - .SetFlags(DamageFlags.IgnoreIFrame) + .SetFlags(DamageFlags.IgnoreIFrame | DamageFlags.IsDoT) .Build(); Owner?.ApplyDirectDamage(info); } diff --git a/Assets/_Game/Scripts/Combat/StatusEffects/StatusEffectManager.cs b/Assets/_Game/Scripts/Combat/StatusEffects/StatusEffectManager.cs index 853d81a..9d0e0b1 100644 --- a/Assets/_Game/Scripts/Combat/StatusEffects/StatusEffectManager.cs +++ b/Assets/_Game/Scripts/Combat/StatusEffects/StatusEffectManager.cs @@ -130,6 +130,9 @@ namespace BaseGames.Combat.StatusEffects /// public void ApplyDirectDamage(DamageInfo info) { + // DoT 绕过 HurtBox,不经过其防御减免步骤,需在此自行结算最终伤害。 + // True 伤害不计防御,直接以 Amount 作为 FinalDamage(Builder 未设置 FinalDamage,默认 0)。 + if (info.FinalDamage <= 0) info.FinalDamage = info.Amount; _damageable?.TakeDamage(info); } diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index 0c7baf6..9fba025 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -159,6 +159,14 @@ namespace BaseGames.Enemies return; } + // ── 持续伤害(DoT):只扣血与受击反馈,不打断/不重置硬直状态 ── + // 否则燃烧/中毒每个 Tick 都会把敌人重新打入 Hurt 并 InterruptAll,形成硬直锁。 + if (info.Flags.HasFlag(DamageFlags.IsDoT)) + { + OnDamageTaken(info); + return; + } + // ── 受击分级(KnockUp > Stagger > Hurt)────────────────────── PoiseLevel curPoise = _poiseSource?.GetCurrentPoiseLevel() ?? PoiseLevel.None; bool causesStagger = info.Flags.HasFlag(DamageFlags.ForceBreak) diff --git a/Assets/_Game/Scripts/Enemies/StatusEffects/StatusEffects.cs b/Assets/_Game/Scripts/Enemies/StatusEffects/StatusEffects.cs index e8b5163..67e945c 100644 --- a/Assets/_Game/Scripts/Enemies/StatusEffects/StatusEffects.cs +++ b/Assets/_Game/Scripts/Enemies/StatusEffects/StatusEffects.cs @@ -116,7 +116,8 @@ namespace BaseGames.Enemies.StatusEffects Amount = (int)_damagePerTick, FinalDamage = (int)_damagePerTick, Type = DamageType.Fire, - Flags = DamageFlags.None, + // IsDoT:持续伤害只扣血,不让每个 Tick 都把敌人重新打入 Hurt/InterruptAll(避免硬直锁) + Flags = DamageFlags.IsDoT, }; enemy.TakeDamage(info); } diff --git a/Assets/_Game/Scripts/Player/PlayerStats.cs b/Assets/_Game/Scripts/Player/PlayerStats.cs index e1b1d18..0cb0e51 100644 --- a/Assets/_Game/Scripts/Player/PlayerStats.cs +++ b/Assets/_Game/Scripts/Player/PlayerStats.cs @@ -192,9 +192,23 @@ namespace BaseGames.Player /// Debug:开启/关闭无敌模式(不计入无敌帧,永久生效直至关闭)。 public void SetGodMode(bool v) { _isGodMode = v; } + /// + /// 受伤入口(含无敌帧判定)。供绕过 HurtBox 的简单环境伤害(如 World 危险区 HazardZone)直接调用。 + /// 经 HurtBox / PlayerController 流水线的伤害已在上游完成 flag 感知的无敌判定,应调用 。 + /// public void TakeDamage(int amount) { - if (_isGodMode || IsInvincible || !IsAlive || amount <= 0) return; + if (IsInvincible) return; + ApplyDamage(amount); + } + + /// + /// 原始扣血(不做无敌判定)。无敌是否生效由上游单一闸门(PlayerController.TakeDamage(DamageInfo)) + /// 依据 DamageFlags.IgnoreIFrame 决定;此处只负责扣血与广播。GodMode 仍然豁免。 + /// + public void ApplyDamage(int amount) + { + if (_isGodMode || !IsAlive || amount <= 0) return; CurrentHP = Mathf.Max(0, CurrentHP - amount); _onHPChanged?.Raise(CurrentHP); OnDamaged?.Invoke(); diff --git a/Assets/_Game/Scripts/Player/States/PlayerController.cs b/Assets/_Game/Scripts/Player/States/PlayerController.cs index 6585c77..66653d5 100644 --- a/Assets/_Game/Scripts/Player/States/PlayerController.cs +++ b/Assets/_Game/Scripts/Player/States/PlayerController.cs @@ -94,23 +94,30 @@ namespace BaseGames.Player.States public void TakeDamage(DamageInfo info) { - if (_stats == null) return; - _stats.TakeDamage(info.FinalDamage); + if (_stats == null || !_stats.IsAlive) return; - // 当前状态标记为无敌(如旧版冲刺状态),或 Stats 层无敌窗口仍激活 - // (冲刺无敌帧 DashInvincibilityDuration 内:跳过受击硬直;窗口过期后可被打断) - if (_currentState?.IsInvincible == true || (_stats != null && _stats.IsInvincible)) return; + // ── 唯一无敌闸门(认 flag)────────────────────────────────────── + // 状态级无敌(冲刺)或 Stats 级无敌窗口激活时,普通伤害被挡; + // 携带 IgnoreIFrame 的伤害(DoT / 陷阱 / 环境)穿透无敌。 + bool invincible = (_currentState?.IsInvincible == true) || _stats.IsInvincible; + if (invincible && !info.Flags.HasFlag(DamageFlags.IgnoreIFrame)) return; - if (_stats.IsAlive) - { - GetState()?.Initialize(info); - TransitionTo(GetState()); - } - else + // 原始扣血(无敌判定已在上面完成,避免下游再做 flag-blind 拦截) + _stats.ApplyDamage(info.FinalDamage); + + if (!_stats.IsAlive) { TransitionTo(GetState()); _onPlayerDied?.Raise(); + return; } + + // ── 持续伤害(DoT):只扣血,不打断硬直、不授予无敌帧 ────────── + if (info.Flags.HasFlag(DamageFlags.IsDoT)) return; + + // 一次性打击:进入受击硬直(HurtState.OnStateEnter 内授予慈悲无敌帧) + GetState()?.Initialize(info); + TransitionTo(GetState()); } // ── IPoiseSource 实现(架构 06_CombatModule §13)─────────────────────