using System.Collections.Generic; using Platformer.Gameplay; using UnityEngine; using static Platformer.Core.Simulation; namespace Platformer.Mechanics { /// /// 플레이어 근거리 공격 판정 박스. /// PlayerAttack 이벤트에서 Fire(direction)를 호출하면 지정 활성 지속 시간 동안 /// OverlapBox 로 적을 감지하고, Health 보유 적에 Decrement 적용 → EnemyDeath 체인. /// 기획 04 §5-1 근거리 공격 1종 — 쿨타임·대미지·판정 박스는 Phase 3-B 튠 대상. /// 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 alreadyHit = new HashSet(); /// /// PlayerAttack.Execute 에서 호출. direction은 플레이어 facing (x축 ±1 or 0). /// 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(); if (health == null || !health.IsAlive) continue; if (alreadyHit.Contains(health)) continue; alreadyHit.Add(health); for (int i = 0; i < damage; i++) { health.Decrement(); if (!health.IsAlive) break; } // Enemy 즉사 시 EnemyController 기반 EnemyDeath 체인 발동 (선택) if (!health.IsAlive) { var enemy = col.GetComponent(); if (enemy != null) { Schedule().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); } } }