chore: initial commit
This commit is contained in:
17
Assets/Scripts/Combat/BaseGames.Combat.asmdef
Normal file
17
Assets/Scripts/Combat/BaseGames.Combat.asmdef
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Combat",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Combat",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Parry"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Combat/BaseGames.Combat.asmdef.meta
Normal file
7
Assets/Scripts/Combat/BaseGames.Combat.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8746e0f9f33d5d84ea0b598962cc36ae
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
82
Assets/Scripts/Combat/CombatEnums.cs
Normal file
82
Assets/Scripts/Combat/CombatEnums.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
// ── 元素/物理属性 ───────────────────────────────────────────────────────
|
||||
public enum DamageType { Normal, True, Fire, Poison, Ice, Lightning, Void }
|
||||
|
||||
// ── 来源分类 ────────────────────────────────────────────────────────────
|
||||
public enum DamageCategory
|
||||
{
|
||||
NormalAttack = 0,
|
||||
SoulSkill = 1,
|
||||
SpiritSkill = 2,
|
||||
Projectile = 3,
|
||||
EnvironmentTrap = 4,
|
||||
StatusEffect = 5,
|
||||
FallDamage = 6,
|
||||
Reflected = 7,
|
||||
}
|
||||
|
||||
// ── 行为标志 ────────────────────────────────────────────────────────────
|
||||
[Flags]
|
||||
public enum DamageFlags
|
||||
{
|
||||
None = 0,
|
||||
Unblockable = 1 << 0,
|
||||
CanBeParried = 1 << 1,
|
||||
IgnoreIFrame = 1 << 2,
|
||||
PerfectParryOnly = 1 << 3,
|
||||
IsProjectile = 1 << 4,
|
||||
CanClash = 1 << 5,
|
||||
ForceBreak = 1 << 6,
|
||||
NoKnockback = 1 << 7,
|
||||
}
|
||||
|
||||
// ── 交互标签 ────────────────────────────────────────────────────────────
|
||||
[Flags]
|
||||
public enum DamageTags : uint
|
||||
{
|
||||
None = 0,
|
||||
MeleeHit = 1 << 0,
|
||||
RangedHit = 1 << 1,
|
||||
SkillHit = 1 << 2,
|
||||
ElementFire = 1 << 3,
|
||||
ElementPoison = 1 << 4,
|
||||
ElementVoid = 1 << 5,
|
||||
AfterParry = 1 << 6,
|
||||
ChargedAttack = 1 << 7,
|
||||
SkyFormOnly = 1 << 8,
|
||||
EarthFormOnly = 1 << 9,
|
||||
DeathFormOnly = 1 << 10,
|
||||
BreakLight = 1 << 11,
|
||||
BreakMedium = 1 << 12,
|
||||
BreakHeavy = 1 << 13,
|
||||
BreakBreaker = 1 << 14,
|
||||
}
|
||||
|
||||
public enum HitFxType { Spark, Slash, Blood, Magic, Heavy, Crit, Void, Heal, Parry, Fire, Ice }
|
||||
|
||||
// ── 攻击方打断等级 ──────────────────────────────────────────────────────
|
||||
public enum BreakLevel
|
||||
{
|
||||
None = 0,
|
||||
Light = 1,
|
||||
Medium = 2,
|
||||
Heavy = 3,
|
||||
Breaker = 4,
|
||||
}
|
||||
|
||||
// ── 承受方霸体等级 ──────────────────────────────────────────────────────
|
||||
public enum PoiseLevel
|
||||
{
|
||||
None = 0,
|
||||
Light = 1,
|
||||
Medium = 2,
|
||||
Heavy = 3,
|
||||
Unbreakable = 100,
|
||||
}
|
||||
|
||||
// ── 攻击方向(PlayerCombat / WeaponSO 使用)────────────────────────────
|
||||
public enum AttackDirection { Ground, Up, Down, Air }
|
||||
}
|
||||
11
Assets/Scripts/Combat/CombatEnums.cs.meta
Normal file
11
Assets/Scripts/Combat/CombatEnums.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca3858c58d2156f4fbc2d295c444bd40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/Scripts/Combat/CombatInterfaces.cs
Normal file
48
Assets/Scripts/Combat/CombatInterfaces.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 可受击实体接口。PlayerController 和 EnemyBase 实现此接口。
|
||||
/// HurtBox.Awake 通过 GetComponentInParent<IDamageable>() 注入。
|
||||
/// </summary>
|
||||
public interface IDamageable
|
||||
{
|
||||
bool IsInvincible { get; }
|
||||
int Defense { get; }
|
||||
void TakeDamage(DamageInfo info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可持有霸体的实体接口。HurtBox 在 ReceiveDamage 中做等级比较。
|
||||
/// </summary>
|
||||
public interface IPoiseSource
|
||||
{
|
||||
PoiseLevel GetCurrentPoiseLevel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 护盾接口(玩家专属)。由 PlayerController.Awake() 注入 HurtBox。
|
||||
/// AbsorbDamage 返回穿透量(0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
|
||||
/// </summary>
|
||||
public interface IShieldable
|
||||
{
|
||||
bool HasShield { get; }
|
||||
int AbsorbDamage(int amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可破坏机关/障碍物接口。HitBox 在命中非 HurtBox 对象时尝试调用。
|
||||
/// </summary>
|
||||
public interface IBreakable
|
||||
{
|
||||
void TryInteract(DamageInfo info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可施加状态效果的实体接口(避免 Combat 直接引用 StatusEffects 程序集)。
|
||||
/// StatusEffectManager 实现此接口;HurtBox.ReceiveDamage 步骤 8 通过此接口调用。
|
||||
/// </summary>
|
||||
public interface IStatusEffectable
|
||||
{
|
||||
void ApplyStatusEffect(DamageType type);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/CombatInterfaces.cs.meta
Normal file
11
Assets/Scripts/Combat/CombatInterfaces.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7881dd1e194f2944a87ec5d50686740e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
82
Assets/Scripts/Combat/DamageInfo.cs
Normal file
82
Assets/Scripts/Combat/DamageInfo.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 单次伤害信息。流水线:RawDamage → Amount(护盾修改)→ FinalDamage(防御减免后)。
|
||||
/// ⚠️ 非 readonly struct — Builder 就地写入字段。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct DamageInfo
|
||||
{
|
||||
public int RawDamage; // HitBox 设定的原始值(Builder.SetRaw 写入一次)
|
||||
public int Amount; // 流水线中被护盾/防御修改
|
||||
public int FinalDamage; // HurtBox 写入,最终 HP 扣除量
|
||||
public Vector2 KnockbackDirection;
|
||||
public float KnockbackForce;
|
||||
public float HitStunDuration;
|
||||
public DamageType Type;
|
||||
public DamageCategory Category;
|
||||
public DamageFlags Flags;
|
||||
public DamageTags Tags;
|
||||
public Vector2 SourcePosition;
|
||||
public int SourceLayer;
|
||||
public HitFxType FxType;
|
||||
public BreakLevel Break;
|
||||
public string SourceId;
|
||||
public string SkillId;
|
||||
|
||||
// ── Builder ──────────────────────────────────────────────────────────
|
||||
public class Builder
|
||||
{
|
||||
private DamageInfo _d;
|
||||
|
||||
public Builder() { }
|
||||
|
||||
// SetRaw 同步初始化 Amount(Amount 始终以 RawDamage 为起点)
|
||||
public Builder SetRaw(int v) { _d.RawDamage = v; _d.Amount = v; return this; }
|
||||
public Builder SetType(DamageType v) { _d.Type = v; return this; }
|
||||
public Builder SetCategory(DamageCategory v){ _d.Category = v; return this; }
|
||||
public Builder SetFlags(DamageFlags v) { _d.Flags = v; return this; }
|
||||
public Builder SetTags(DamageTags v) { _d.Tags = v; return this; }
|
||||
public Builder SetSkillId(string v) { _d.SkillId = v; return this; }
|
||||
public Builder SetSourceId(string v) { _d.SourceId = v; return this; }
|
||||
public Builder SetKnockback(Vector2 dir, float force)
|
||||
{ _d.KnockbackDirection = dir; _d.KnockbackForce = force; return this; }
|
||||
public Builder SetStun(float dur) { _d.HitStunDuration = dur; return this; }
|
||||
public Builder SetFx(HitFxType v) { _d.FxType = v; return this; }
|
||||
public Builder SetBreak(BreakLevel v) { _d.Break = v; return this; }
|
||||
public Builder SetSourcePos(Vector2 v) { _d.SourcePosition = v; return this; }
|
||||
public Builder SetLayer(int v) { _d.SourceLayer = v; return this; }
|
||||
public DamageInfo Build() => _d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ⚡ 零堆分配工厂(热路径首选)。直接从 DamageSourceSO 填入基础字段。
|
||||
/// KnockbackDirection / SourcePosition / SourceLayer 等运行时字段由调用方就地赋值。
|
||||
/// </summary>
|
||||
public static DamageInfo From(DamageSourceSO so)
|
||||
{
|
||||
int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier);
|
||||
return new DamageInfo
|
||||
{
|
||||
RawDamage = baseAmt,
|
||||
Amount = baseAmt,
|
||||
Type = so.Type,
|
||||
Category = so.Category,
|
||||
Flags = so.Flags,
|
||||
Tags = so.Tags,
|
||||
HitStunDuration = so.HitStunDuration,
|
||||
FxType = so.FxType,
|
||||
Break = so.BreakLevel,
|
||||
SourceId = so.sourceId,
|
||||
SkillId = so.skillId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>伤害事件频道(EVT_DamageDealt)。</summary>
|
||||
[UnityEngine.CreateAssetMenu(menuName = "Events/DamageDealt")]
|
||||
public class DamageInfoEventChannelSO : BaseEventChannelSO<DamageInfo> { }
|
||||
}
|
||||
11
Assets/Scripts/Combat/DamageInfo.cs.meta
Normal file
11
Assets/Scripts/Combat/DamageInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78a4c2420f838e74aa97697d5da09b72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
52
Assets/Scripts/Combat/DamageSourceSO.cs
Normal file
52
Assets/Scripts/Combat/DamageSourceSO.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击数据源 SO。描述单次攻击的基础伤害参数。
|
||||
/// ⚡ 热路径使用零分配工厂:DamageInfo.From(sourceSO)。
|
||||
/// 仅需链式覆盖多字段时才使用 CreateBuilder()。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Combat/DamageSource")]
|
||||
public class DamageSourceSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string sourceId;
|
||||
public string skillId;
|
||||
|
||||
[Header("Base")]
|
||||
public int BaseDamage = 10;
|
||||
public float DamageMultiplier = 1.0f;
|
||||
public DamageType Type = DamageType.Normal;
|
||||
public DamageCategory Category = DamageCategory.NormalAttack;
|
||||
public DamageFlags Flags = DamageFlags.CanBeParried;
|
||||
public DamageTags Tags = DamageTags.MeleeHit;
|
||||
|
||||
[Header("Physics")]
|
||||
public float KnockbackForce = 5f;
|
||||
public float HitStunDuration = 0.1f;
|
||||
public BreakLevel BreakLevel = BreakLevel.Light;
|
||||
|
||||
[Header("FX")]
|
||||
public HitFxType FxType = HitFxType.Slash;
|
||||
|
||||
[Header("Combo")]
|
||||
public float ComboWindowDuration = 0.4f;
|
||||
public float CancelWindowEnd = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// 链式 Builder(特殊场景使用,热路径改用 DamageInfo.From(this))。
|
||||
/// </summary>
|
||||
public DamageInfo.Builder CreateBuilder() => new DamageInfo.Builder()
|
||||
.SetRaw(Mathf.RoundToInt(BaseDamage * DamageMultiplier))
|
||||
.SetType(Type)
|
||||
.SetCategory(Category)
|
||||
.SetFlags(Flags)
|
||||
.SetTags(Tags)
|
||||
.SetStun(HitStunDuration)
|
||||
.SetFx(FxType)
|
||||
.SetBreak(BreakLevel)
|
||||
.SetSourceId(sourceId)
|
||||
.SetSkillId(skillId);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/DamageSourceSO.cs.meta
Normal file
11
Assets/Scripts/Combat/DamageSourceSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96b10c11e6173394a8fa8d9c614b0035
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
96
Assets/Scripts/Combat/HitBox.cs
Normal file
96
Assets/Scripts/Combat/HitBox.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
|
||||
/// Phase 1 简化:直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
|
||||
/// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class HitBox : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private DamageSourceSO _defaultSource;
|
||||
[SerializeField] private float _hitCooldown = 0.1f;
|
||||
|
||||
private DamageSourceSO _currentSource;
|
||||
private Transform _attackerTransform;
|
||||
private bool _isActive;
|
||||
|
||||
/// <summary>命中确认委托(PlayerCombat / EnemyCombat 订阅)。</summary>
|
||||
public System.Action<DamageInfo> OnHitConfirmed;
|
||||
|
||||
/// <summary>
|
||||
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
|
||||
/// ⚠️ 不存在 Activate(float duration) 重载。
|
||||
/// </summary>
|
||||
public void Activate(DamageSourceSO source = null, Transform attacker = null)
|
||||
{
|
||||
_currentSource = source ?? _defaultSource;
|
||||
_attackerTransform = attacker ?? transform;
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
public void Deactivate() => _isActive = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 确保 Collider2D 是 Trigger
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (!col.isTrigger)
|
||||
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_isActive = false;
|
||||
_hitCooldownTimers.Clear();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!_isActive) return;
|
||||
if (_currentSource == null)
|
||||
{
|
||||
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this);
|
||||
return;
|
||||
}
|
||||
if (!CheckCooldown(other)) return;
|
||||
|
||||
Vector2 knockDir = ((Vector2)other.bounds.center
|
||||
- (Vector2)_attackerTransform.position).normalized;
|
||||
|
||||
// ⚡ 零 GC:struct 工厂,就地赋值运行时字段
|
||||
var info = DamageInfo.From(_currentSource);
|
||||
info.KnockbackDirection = knockDir;
|
||||
info.KnockbackForce = _currentSource.KnockbackForce;
|
||||
info.SourcePosition = _attackerTransform.position;
|
||||
info.SourceLayer = _attackerTransform.gameObject.layer;
|
||||
|
||||
// ① 命中 HurtBox
|
||||
var hurtBox = other.GetComponent<HurtBox>();
|
||||
if (hurtBox != null)
|
||||
{
|
||||
hurtBox.ReceiveDamage(info);
|
||||
OnHitConfirmed?.Invoke(info);
|
||||
return;
|
||||
}
|
||||
|
||||
// ② 命中 IBreakable(机关/障碍物)
|
||||
other.GetComponent<IBreakable>()?.TryInteract(info);
|
||||
}
|
||||
|
||||
// ── 同目标多帧命中冷却 ────────────────────────────────────────────────
|
||||
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new();
|
||||
|
||||
private bool CheckCooldown(Collider2D other)
|
||||
{
|
||||
float now = Time.time;
|
||||
if (_hitCooldownTimers.TryGetValue(other, out float last) && now - last < _hitCooldown)
|
||||
return false;
|
||||
_hitCooldownTimers[other] = now;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitBox.cs.meta
Normal file
11
Assets/Scripts/Combat/HitBox.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a655e2461396a8348a32a13144438e8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs
Normal file
8
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Events/HitConfirmed")]
|
||||
public class HitConfirmedEventChannelSO : BaseEventChannelSO<HitInfo> { }
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs.meta
Normal file
11
Assets/Scripts/Combat/HitConfirmedEventChannelSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e5ffa3ce0537845b1b601c267d76ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
17
Assets/Scripts/Combat/HitInfo.cs
Normal file
17
Assets/Scripts/Combat/HitInfo.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 命中信息(HurtBox.ReceiveDamage 广播给 VFX/Audio/Feedback)。
|
||||
/// </summary>
|
||||
public struct HitInfo
|
||||
{
|
||||
public DamageInfo DamageInfo;
|
||||
public Vector3 HitPoint;
|
||||
public Vector3 HitNormal;
|
||||
public Transform HitTransform;
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Combat/HitInfo.cs.meta
Normal file
11
Assets/Scripts/Combat/HitInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5933018bd81bef48be815337eef02af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
106
Assets/Scripts/Combat/HurtBox.cs
Normal file
106
Assets/Scripts/Combat/HurtBox.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Parry;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 受击盒组件。实现完整 8 步伤害流水线(架构 06_CombatModule §5)。
|
||||
/// 挂载在角色根节点或指定子节点上,Collider2D 需设 IsTrigger = true,
|
||||
/// Layer = PlayerHurtBox 或 EnemyHurtBox。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class HurtBox : MonoBehaviour
|
||||
{
|
||||
// ── 伤害接受方(Awake 注入)──────────────────────────────────────────
|
||||
private IDamageable _owner;
|
||||
private IShieldable _shieldable; // 由 PlayerController.Awake() 注入
|
||||
private ParrySystem _parrySystem; // Phase 2 由 PlayerController.Awake() 注入
|
||||
private IPoiseSource _poiseSource; // Phase 2 由 EnemyBase.Awake() 注入
|
||||
|
||||
private bool _isHurtBoxInvincible;
|
||||
private bool _isActive = true;
|
||||
|
||||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||||
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
|
||||
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
|
||||
|
||||
// ── 注入接口 ──────────────────────────────────────────────────────────
|
||||
public void SetShieldable(IShieldable shieldable) => _shieldable = shieldable;
|
||||
public void SetParrySystem(ParrySystem ps) => _parrySystem = ps;
|
||||
public void SetPoiseSource(IPoiseSource src) => _poiseSource = src;
|
||||
public void SetInvincible(bool value) => _isHurtBoxInvincible = value;
|
||||
public void SetActive(bool value) => _isActive = value;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_owner = GetComponentInParent<IDamageable>();
|
||||
if (_owner == null)
|
||||
Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接受伤害(由 HitBox.OnTriggerEnter2D 直接调用)。
|
||||
/// ⚠️ 方法名必须为 ReceiveDamage。
|
||||
/// </summary>
|
||||
public void ReceiveDamage(DamageInfo info)
|
||||
{
|
||||
if (!_isActive || _owner == null) return;
|
||||
|
||||
// 1. 无敌帧检查
|
||||
if ((_owner.IsInvincible || _isHurtBoxInvincible)
|
||||
&& !info.Flags.HasFlag(DamageFlags.IgnoreIFrame)) return;
|
||||
|
||||
// 2. 弹反检查(Phase 1 _parrySystem == null 跳过)
|
||||
// ParrySystem 只暴露窗口状态,伤害数据留在 Combat 层,无跨程序集数据依赖。
|
||||
if (_parrySystem != null && info.Flags.HasFlag(DamageFlags.CanBeParried))
|
||||
if (_parrySystem.ConsumeParry()) return;
|
||||
|
||||
// 3. 霸体检查(Phase 1 _poiseSource == null 跳过)
|
||||
if (!info.Flags.HasFlag(DamageFlags.ForceBreak) && _poiseSource != null)
|
||||
{
|
||||
PoiseLevel curPoise = _poiseSource.GetCurrentPoiseLevel();
|
||||
if (curPoise == PoiseLevel.Unbreakable) return;
|
||||
if ((int)info.Break < (int)curPoise)
|
||||
{
|
||||
_onHitConfirmed?.Raise(new HitInfo
|
||||
{
|
||||
DamageInfo = info,
|
||||
HitPoint = transform.position,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 护盾层拦截(玩家专属,在防御减免前)
|
||||
if (_shieldable != null && _shieldable.HasShield)
|
||||
{
|
||||
int passThrough = _shieldable.AbsorbDamage(info.Amount);
|
||||
if (passThrough <= 0) return;
|
||||
info.Amount = passThrough;
|
||||
}
|
||||
|
||||
// 5. 计算 FinalDamage(防御减免,最低 1)
|
||||
int finalDamage = UnityEngine.Mathf.Max(1, info.Amount - _owner.Defense);
|
||||
info.Amount = finalDamage;
|
||||
info.FinalDamage = finalDamage;
|
||||
|
||||
// 6. 调用 _owner.TakeDamage
|
||||
_owner.TakeDamage(info);
|
||||
|
||||
// 7. 全局广播
|
||||
_onDamageDealt?.Raise(info);
|
||||
_onHitConfirmed?.Raise(new HitInfo
|
||||
{
|
||||
DamageInfo = info,
|
||||
HitPoint = transform.position,
|
||||
});
|
||||
|
||||
// 8. 状态效果触发(DoT — Fire / Poison)
|
||||
// 使用接口避免对 StatusEffects 程序集的直接依赖
|
||||
if (_owner is UnityEngine.MonoBehaviour mb)
|
||||
{
|
||||
mb.GetComponent<IStatusEffectable>()?.ApplyStatusEffect(info.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/HurtBox.cs.meta
Normal file
11
Assets/Scripts/Combat/HurtBox.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b7a233d7f70aa4f86b473412b826de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Scripts/Combat/ShieldComponent.cs
Normal file
20
Assets/Scripts/Combat/ShieldComponent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 护盾组件(Phase 1 存根)。实现 IShieldable 接口供 HurtBox 注入。
|
||||
/// Phase 2 实现完整护盾逻辑(护盾值、再生、破盾事件)。
|
||||
/// </summary>
|
||||
public class ShieldComponent : MonoBehaviour, IShieldable
|
||||
{
|
||||
public bool HasShield { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尝试以护盾吸收伤害。
|
||||
/// 返回穿透量(0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
|
||||
/// Phase 1:护盾不存在,全量穿透。
|
||||
/// </summary>
|
||||
public int AbsorbDamage(int amount) => amount;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Combat/ShieldComponent.cs.meta
Normal file
11
Assets/Scripts/Combat/ShieldComponent.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f362045054d7c1945841c4ccbcb356e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Combat/StatusEffects.meta
Normal file
8
Assets/Scripts/Combat/StatusEffects.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37363fb905d771d45b74c104305f07dd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Combat/StatusEffects/.gitkeep
Normal file
0
Assets/Scripts/Combat/StatusEffects/.gitkeep
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Combat.StatusEffects",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Combat.StatusEffects",
|
||||
"references": [
|
||||
"BaseGames.Combat"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd947c06a464c1b4492d0417d28a8ccb
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs
Normal file
20
Assets/Scripts/Combat/StatusEffects/StatusEffectManager.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Combat.StatusEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态效果管理器(Phase 1 桩)。
|
||||
/// 实现 IStatusEffectable 接口,由 HurtBox 通过接口调用,避免程序集循环依赖。
|
||||
/// Phase 2 实现完整的效果叠加、持续时间、DoT 伤害计算。
|
||||
/// </summary>
|
||||
public class StatusEffectManager : MonoBehaviour, IStatusEffectable
|
||||
{
|
||||
// Phase 1:空实现
|
||||
public void ApplyStatusEffect(DamageType type) { }
|
||||
}
|
||||
|
||||
// ── Phase 1 占位效果类型 ──────────────────────────────────────────────────
|
||||
public class FireEffect { }
|
||||
public class PoisonEffect { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 708938b7c3d75b244abcbd30ed589461
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs
Normal file
3
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Combat.StatusEffects { }
|
||||
|
||||
11
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Combat/StatusEffects/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1dfc988231a6ac14a9aa035ba1719ab0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Combat/_Placeholder.cs
Normal file
3
Assets/Scripts/Combat/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Combat { }
|
||||
|
||||
11
Assets/Scripts/Combat/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Combat/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9c4356cd693b604bb0889f9538eb13e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user