EerieVillage/Assets/Scripts/Mechanics/AttackHitbox.cs

93 lines
3.9 KiB
C#

using System.Collections.Generic;
using Platformer.Gameplay;
using UnityEngine;
using static Platformer.Core.Simulation;
namespace Platformer.Mechanics
{
/// <summary>
/// 플레이어 근거리 공격 판정 박스.
/// PlayerAttack 이벤트에서 Fire(direction)를 호출하면 지정 활성 지속 시간 동안
/// OverlapBox 로 적을 감지하고, Health 보유 적에 Decrement 적용 → EnemyDeath 체인.
/// 기획 04 §5-1 근거리 공격 1종 — 쿨타임·대미지·판정 박스는 Phase 3-B 튠 대상.
/// </summary>
public class AttackHitbox : MonoBehaviour
{
[Header("판정 박스 크기 (플레이어 기준 로컬)")]
public Vector2 size = new Vector2(1.2f, 0.9f);
[Tooltip("플레이어 중심으로부터 공격 방향으로의 오프셋 거리")]
public float offsetDistance = 0.7f;
[Tooltip("판정 활성 지속 시간 (초). 정적 스프라이트 기반이면 짧게 유지")]
public float activeDuration = 0.12f;
[Tooltip("대미지 (Health.Decrement 호출 횟수)")]
public int damage = 1;
[Header("타격 대상 레이어 마스크")]
public LayerMask targetLayers = ~0; // 전 레이어 기본. 실전에서 Enemy 레이어로 제한 권장
float activeUntil = -1f;
Vector2 lastDirection = Vector2.right;
// 같은 스윙으로 동일 Health 중복 타격 방지
readonly HashSet<Health> alreadyHit = new HashSet<Health>();
/// <summary>
/// PlayerAttack.Execute 에서 호출. direction은 플레이어 facing (x축 ±1 or 0).
/// </summary>
public void Fire(Vector2 direction)
{
if (Mathf.Abs(direction.x) > 0.01f) lastDirection = new Vector2(Mathf.Sign(direction.x), 0);
activeUntil = Time.time + activeDuration;
alreadyHit.Clear();
}
void Update()
{
if (Time.time > activeUntil) return;
// 로컬 오프셋: 플레이어 중심 + facing * offsetDistance
var center = (Vector2)transform.position + lastDirection * offsetDistance;
// OverlapBox로 적 검출
var hits = Physics2D.OverlapBoxAll(center, size, 0f, targetLayers);
foreach (var col in hits)
{
if (col == null) continue;
// 자기 자신 collider 제외 (PlayerController 부착 GameObject)
if (col.transform == transform || col.transform.IsChildOf(transform)) continue;
var health = col.GetComponent<Health>();
if (health == null || !health.IsAlive) continue;
if (alreadyHit.Contains(health)) continue;
alreadyHit.Add(health);
// BT7-Plan TODO (2026-04-24): 적 Health도 하트 분할 시스템 대상이지만
// i-frame 구조 탓에 Decrement() 반복 호출은 첫 호출 후 무효화된다.
// Health.Decrement(int damage) 단일 호출로 쿼터 단위 다중 피해 전달.
health.Decrement(damage);
// Enemy 즉사 시 EnemyController 기반 EnemyDeath 체인 발동 (선택)
if (!health.IsAlive)
{
var enemy = col.GetComponent<EnemyController>();
if (enemy != null)
{
Schedule<EnemyDeath>().enemy = enemy;
}
}
}
}
// Scene view 가시화 — 활성 상태에서 박스 표시
void OnDrawGizmos()
{
Gizmos.color = (Application.isPlaying && Time.time <= activeUntil)
? new Color(1f, 0.3f, 0.3f, 0.6f)
: new Color(1f, 1f, 1f, 0.2f);
var dir = Application.isPlaying ? lastDirection : Vector2.right;
var center = (Vector2)transform.position + dir * offsetDistance;
Gizmos.DrawWireCube(center, size);
}
}
}