using System.Collections.Generic; using UnityEngine; using Colosseum; using Colosseum.Weapons; namespace Colosseum.Skills { /// /// 스킬 효과의 기본 클래스. /// 모든 효과는 이 클래스를 상속받아 구현합니다. /// 효과 발동 타이밍은 애니메이션 이벤트(OnEffect)로 제어합니다. /// public abstract class SkillEffect : ScriptableObject { [Header("대상 설정")] [Tooltip("Self: 시전자, Area: 범위 내 대상")] [SerializeField] protected TargetType targetType = TargetType.Self; [Header("Area Settings (TargetType이 Area일 때)")] [Tooltip("범위 내에서 공격할 대상 필터")] [SerializeField] protected TargetTeam targetTeam = TargetTeam.Enemy; [SerializeField] protected AreaCenterType areaCenter = AreaCenterType.Caster; [SerializeField] protected AreaShapeType areaShape = AreaShapeType.Sphere; [SerializeField] protected LayerMask targetLayers; [Header("Sphere Settings")] [Min(0.1f)] [SerializeField] protected float areaRadius = 3f; [Header("Fan Settings")] [Tooltip("부채꼴 원점이 캐릭터로부터 떨어진 거리")] [Min(0f)] [SerializeField] protected float fanOriginDistance = 1f; [Tooltip("부채꼴 반지름")] [Min(0.1f)] [SerializeField] protected float fanRadius = 3f; [Tooltip("부채꼴 좌우 각도 (각 방향으로 이 각도만큼 벌어짐, 총 각도 = 2배)")] [Range(0f, 180f)] [SerializeField] protected float fanHalfAngle = 45f; // Properties public TargetType TargetType => targetType; public TargetTeam TargetTeam => targetTeam; public AreaShapeType AreaShape => areaShape; public float AreaRadius => areaRadius; public float FanOriginDistance => fanOriginDistance; public float FanRadius => fanRadius; public float FanHalfAngle => fanHalfAngle; /// /// 스킬 시전 시 호출 /// public void ExecuteOnCast(GameObject caster) { switch (targetType) { case TargetType.Self: ApplyEffect(caster, caster); break; case TargetType.Area: ExecuteArea(caster); break; } } /// /// 투사체 충돌 시 호출 /// public void ExecuteOnHit(GameObject caster, GameObject hitTarget) { if (IsValidTarget(caster, hitTarget)) { ApplyEffect(caster, hitTarget); } } /// /// 실제 효과 적용 (상속받은 클래스에서 구현) /// protected abstract void ApplyEffect(GameObject caster, GameObject target); /// /// 충돌한 대상이 유효한 타겟인지 확인 /// public bool IsValidTarget(GameObject caster, GameObject target) { if (target == null) return false; // 레이어 체크 if (targetLayers.value != 0 && (targetLayers.value & (1 << target.layer)) == 0) return false; // 팀 체크 return IsCorrectTeam(caster, target); } private bool IsCorrectTeam(GameObject caster, GameObject target) { bool isSameTeam = Team.IsSameTeam(caster, target); return targetTeam switch { TargetTeam.Enemy => !isSameTeam, TargetTeam.Ally => isSameTeam, TargetTeam.All => true, _ => true }; } private void ExecuteArea(GameObject caster) { Vector3 center = GetAreaCenter(caster); Collider[] hits = Physics.OverlapSphere(center, Mathf.Max(areaRadius, fanRadius), targetLayers); // 같은 GameObject가 여러 콜라이더를 가질 수 있으므로 중복 제거 HashSet processedTargets = new HashSet(); foreach (var hit in hits) { if (hit.gameObject == caster) continue; if (!IsCorrectTeam(caster, hit.gameObject)) continue; if (processedTargets.Contains(hit.gameObject)) continue; // 부채꼴 판정 if (areaShape == AreaShapeType.Fan) { if (!IsInFanShape(caster, hit.transform.position)) continue; } processedTargets.Add(hit.gameObject); ApplyEffect(caster, hit.gameObject); } } /// /// 타겟이 부채꼴 범위 내에 있는지 확인 /// private bool IsInFanShape(GameObject caster, Vector3 targetPosition) { Vector3 casterPos = caster.transform.position; Vector3 casterForward = caster.transform.forward; // 부채꼴 원점 계산 Vector3 fanOrigin = casterPos + casterForward * fanOriginDistance; // 원점에서 타겟까지의 방향과 거리 Vector3 toTarget = targetPosition - fanOrigin; float distance = toTarget.magnitude; // 거리 체크 if (distance > fanRadius) return false; // 각도 체크 (Y축 무시) Vector3 toTargetFlat = new Vector3(toTarget.x, 0f, toTarget.z).normalized; Vector3 casterForwardFlat = new Vector3(casterForward.x, 0f, casterForward.z).normalized; float angle = Vector3.Angle(casterForwardFlat, toTargetFlat); return angle <= fanHalfAngle; } private Vector3 GetAreaCenter(GameObject caster) { if (caster == null) return Vector3.zero; return areaCenter switch { AreaCenterType.Caster => caster.transform.position, AreaCenterType.CasterForward => caster.transform.position + caster.transform.forward * areaRadius, _ => caster.transform.position }; } #region Debug Visualization /// /// 공격 범위 시각화 (런타임 디버그용) /// public void DrawDebugRange(GameObject caster, float duration = 1f) { if (targetType != TargetType.Area) return; Vector3 casterPos = caster.transform.position; Vector3 forward = caster.transform.forward; if (areaShape == AreaShapeType.Sphere) { Vector3 center = GetAreaCenter(caster); DebugDrawSphere(center, areaRadius, Color.red, duration); } else if (areaShape == AreaShapeType.Fan) { DebugDrawFan(casterPos, forward, fanOriginDistance, fanRadius, fanHalfAngle, Color.red, duration); } } private void DebugDrawSphere(Vector3 center, float radius, Color color, float duration) { int segments = 32; float step = 360f / segments; // XZ 평면 원 Vector3 prevPoint = center + new Vector3(radius, 0, 0); for (int i = 1; i <= segments; i++) { float angle = i * step * Mathf.Deg2Rad; Vector3 newPoint = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); Debug.DrawLine(prevPoint, newPoint, color, duration); prevPoint = newPoint; } // 수직선 (높이 표시) Debug.DrawLine(center + Vector3.up * 0.1f, center + Vector3.up * 2f, color, duration); } private void DebugDrawFan(Vector3 casterPos, Vector3 forward, float originDistance, float radius, float halfAngle, Color color, float duration) { Vector3 fanOrigin = casterPos + forward * originDistance; Vector3 forwardFlat = new Vector3(forward.x, 0f, forward.z).normalized; // 부채꼴의 양끝 방향 계산 Quaternion leftRot = Quaternion.Euler(0, -halfAngle, 0); Quaternion rightRot = Quaternion.Euler(0, halfAngle, 0); Vector3 leftDir = leftRot * forwardFlat; Vector3 rightDir = rightRot * forwardFlat; // 부채꼴 호 그리기 int arcSegments = Mathf.Max(8, Mathf.CeilToInt(halfAngle * 2 / 5f)); // 5도당 1세그먼트 Vector3 prevPoint = fanOrigin + leftDir * radius; for (int i = 1; i <= arcSegments; i++) { float t = (float)i / arcSegments; float angle = -halfAngle + t * halfAngle * 2; Quaternion rot = Quaternion.Euler(0, angle, 0); Vector3 dir = rot * forwardFlat; Vector3 newPoint = fanOrigin + dir * radius; Debug.DrawLine(prevPoint, newPoint, color, duration); prevPoint = newPoint; } // 부채꼴 경계선 Debug.DrawLine(fanOrigin, fanOrigin + leftDir * radius, color, duration); Debug.DrawLine(fanOrigin, fanOrigin + rightDir * radius, color, duration); // 원점 표시 Debug.DrawLine(fanOrigin + Vector3.up * 0.1f, fanOrigin + Vector3.up * 1.5f, color, duration); } #endregion } public enum TargetType { Self, Area } public enum TargetTeam { Enemy, Ally, All } public enum AreaCenterType { Caster, CasterForward } public enum AreaShapeType { Sphere, // 원형 범위 Fan // 부채꼴 범위 } }