using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.Combat
{
///
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
/// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
/// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。
///
[RequireComponent(typeof(Collider2D))]
public class HitBox : MonoBehaviour
{
[SerializeField] private DamageSourceSO _defaultSource;
[SerializeField] private float _hitCooldown = 0.1f;
///
/// HitBox 标识符,供 PlayerAnimationEvents / EnemyAnimationEvents 按名称精确激活特定判定盒。
/// 留空表示"无 Id";事件 payload 为空时将操作所有 HitBox。
///
[SerializeField] private string _id = "";
public string Id => _id;
///
/// 对立阵营 HitBox 所在的 Layer 掩码(用于拼刀检测)。
/// Inspector 中将 PlayerHitBox 与 EnemyHitBox 两个 Layer 均勾选。
///
[SerializeField] private LayerMask _rivalHitBoxMask;
private DamageSourceSO _currentSource;
private Transform _attackerTransform;
private Rigidbody2D _ownerRigidbody;
private bool _isActive;
private IClashService _clashService;
private Collider2D _collider;
/// HitBox 当前是否激活(供 ClashResolver 查询)。
public bool IsActive => _isActive;
/// 当前 Source 是否携带 CanClash 标记(供 ClashResolver 查询)。
public bool CanClash => _currentSource != null && _currentSource.Flags.HasFlag(DamageFlags.CanClash);
/// 宿主角色的 Rigidbody2D(用于拼刀弹开力计算)。
public Rigidbody2D OwnerRigidbody => _ownerRigidbody;
// 拼刀检测所需的对立层掩码(Inspector 配置)
/// 命中确认委托(PlayerCombat / EnemyCombat 订阅)。
public event System.Action OnHitConfirmed;
// 宿主投射物缓存(Activate 时填入,DamageInfo.SourceProjectile 写入用)
private Projectile _ownerProjectile;
///
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
/// ⚠️ 不存在 Activate(float duration) 重载。
///
public void Activate(DamageSourceSO source = null, Transform attacker = null)
{
_currentSource = source ?? _defaultSource;
_attackerTransform = attacker ?? transform;
_isActive = true;
// 缓存宿主 Rigidbody2D(沿父层级向上查找)
_ownerRigidbody = _attackerTransform.GetComponentInParent();
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
public void Deactivate()
{
_isActive = false;
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
/// 仅替换当前 DamageSource(不改变激活状态,供 PlayerCombat 连击段切换)。
public void SetDamageSource(DamageSourceSO source)
{
if (source != null) _currentSource = source;
}
private void Awake()
{
// 确保 Collider2D 是 Trigger
_collider = GetComponent();
if (!_collider.isTrigger)
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
// 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
_clashService = ServiceLocator.GetOrDefault();
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null)
_ownerProjectile = GetComponent();
}
private void OnDisable()
{
_isActive = false;
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
private void OnTriggerExit2D(Collider2D other)
{
// 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等)
// 因有效目标持续流动而无限积累已离场对象。
// 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。
_hitCooldownTimers.Remove(other);
}
private void OnTriggerEnter2D(Collider2D other) {
if (!_isActive) return;
if (_currentSource == null)
{
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this);
return;
}
// 同一激活期防止对同一 Collider 重复命中(一次攻击每个目标至多命中一次)
if (!_hitThisActivation.Add(other)) return;
if (!CheckCooldown(other)) return;
// 排除自身:攻击方与受击方属于同一根 GameObject 时跳过(防止近战 EnemyHitBox 命中同一敌人的 EnemyHurtBox)
if (other.transform.root == _attackerTransform.root) return;
Vector2 knockDir = ((Vector2)other.bounds.center
- (Vector2)_attackerTransform.position).normalized;
// ⚡ 零 GC:struct 工厂,运行时字段内联传入
var info = DamageInfo.From(
_currentSource,
knockDir,
_attackerTransform.position,
_attackerTransform.gameObject.layer,
_ownerProjectile);
// ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层
int otherLayer = other.gameObject.layer;
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
if (isRivalHitBoxLayer && CanClash)
{
var rivalHitBox = other.GetComponent();
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
{
_clashService?.ResolveClash(this, rivalHitBox);
return; // 拼刀,中止伤害流水线
}
}
// ② 命中 HurtBox
var hurtBox = other.GetComponent();
if (hurtBox != null)
{
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
Vector3 hitPoint = other.ClosestPoint(_collider.bounds.center);
hurtBox.ReceiveDamage(info, hitPoint);
OnHitConfirmed?.Invoke(info);
return;
}
// ③ 命中 IBreakable(机关/障碍物)
other.GetComponent()?.TryInteract(info);
}
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
private readonly HashSet _hitThisActivation = new(8);
// ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)──
private readonly Dictionary _hitCooldownTimers = new(8);
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;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
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);
}
// ────────────────────────────────────────────────────
// Gizmo 辅助(内联,不依赖外部工具类)
// ────────────────────────────────────────────────────
///
/// 根据 Collider2D 类型绘制正确的 wire 轮廓。
/// 使用 统一应用 Transform 的移动/旋转/缩放。
///
public static void DrawCollider2DWire(Collider2D col)
{
var prev = Gizmos.matrix;
Gizmos.matrix = col.transform.localToWorldMatrix;
switch (col)
{
case BoxCollider2D box:
DrawWireRect2D(box.offset, box.size);
break;
case CapsuleCollider2D caps:
DrawWireCapsule2D(caps.offset, caps.size, caps.direction);
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);
break;
default:
// 其他类型回退到 bounds 近似(恢复矩阵后在世界空间绘制)
Gizmos.matrix = prev;
DrawWireRect2D(col.bounds.center, col.bounds.size);
return;
}
Gizmos.matrix = prev;
}
/// 在当前 Gizmos 坐标系中绘制轴对齐矩形(2D 线框,兼容透视相机)。
public static void DrawWireRect2D(Vector2 center, Vector2 size)
{
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++)
{
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;
}
}
/// 绘制 2D 胶囊轮廓(在 col.transform 局部坐标系中)。
private static void DrawWireCapsule2D(Vector2 offset, Vector2 size, CapsuleDirection2D dir)
{
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);
}
/// 用多段直线近似绘制 2D 圆弧。
private static void DrawArc2D(Vector2 c, float r, float startDeg, float spanDeg, int segs = 20)
{
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++)
{
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;
}
}
#endif
}
}