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)─────────────────────