using System.Collections.Generic; using UnityEngine; using Colosseum; using Colosseum.Weapons; namespace Colosseum.Skills { /// /// 스킬 효과의 기본 클래스. /// 모든 효과는 이 클래스를 상속받아 구현합니다. /// 효과 발동 타이밍은 애니메이션 이벤트(OnEffect)로 제어합니다. /// public abstract class SkillEffect : ScriptableObject { [Header("대상 설정")] [Tooltip("Self: 시전자, Area: 범위 내 대상, SingleAlly: 아군 1인 (외부에서 타겟 주입)")] [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; [Tooltip("Area 범위 효과일 때 시전자 본인 포함 여부")] [SerializeField] protected bool includeCasterInArea = false; [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 bool IncludeCasterInArea => includeCasterInArea; public AreaCenterType AreaCenter => areaCenter; /// /// 이 효과가 순수 시각 효과인지 확인합니다. /// true이면 서버 가드를 무시하고 모든 클라이언트에서 로컬 실행됩니다. /// public virtual bool IsVisualOnly => false; /// /// 스킬 시전 시 호출 /// public virtual void ExecuteOnCast(GameObject caster, GameObject targetOverride = null, Vector3? groundPosition = null) { List targets = new List(); CollectTargets(caster, targets, targetOverride, groundPosition); for (int i = 0; i < targets.Count; i++) { ApplyEffect(caster, targets[i]); } } /// /// 현재 효과가 영향을 줄 대상 목록을 수집합니다. /// 젬의 적중 이상상태 적용 등에서 동일한 타겟 해석을 재사용하기 위한 경로입니다. /// public void CollectTargets(GameObject caster, List destination, GameObject targetOverride = null, Vector3? groundPosition = null) { if (caster == null || destination == null) return; switch (targetType) { case TargetType.Self: AddUniqueTarget(destination, caster); break; case TargetType.Area: CollectAreaTargets(caster, destination, groundPosition); break; case TargetType.SingleAlly: if (targetOverride != null && IsCorrectTeam(caster, targetOverride)) AddUniqueTarget(destination, targetOverride); else AddUniqueTarget(destination, 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) { // Team 컴포넌트가 없는 오브젝트(환경, 바닥, 벽 등)는 타겟 제외 if (target.GetComponent() == null) return false; bool isSameTeam = Team.IsSameTeam(caster, target); return targetTeam switch { TargetTeam.Enemy => !isSameTeam, TargetTeam.Ally => isSameTeam, TargetTeam.All => true, _ => true }; } private void CollectAreaTargets(GameObject caster, List destination, Vector3? groundPosition = null) { Vector3 center = GetAreaCenter(caster, groundPosition); Collider[] hits = Physics.OverlapSphere(center, Mathf.Max(areaRadius, fanRadius), targetLayers); foreach (var hit in hits) { if (!includeCasterInArea && hit.gameObject == caster) continue; if (!IsCorrectTeam(caster, hit.gameObject)) continue; // 부채꼴 판정 if (areaShape == AreaShapeType.Fan) { if (!IsInFanShape(caster, hit.transform.position)) continue; } AddUniqueTarget(destination, hit.gameObject); } } private static void AddUniqueTarget(List destination, GameObject target) { if (target == null || destination.Contains(target)) return; destination.Add(target); } /// /// 타겟이 부채꼴 범위 내에 있는지 확인 /// 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, Vector3? groundPosition = null) { if (caster == null) return Vector3.zero; return areaCenter switch { AreaCenterType.Caster => caster.transform.position, AreaCenterType.CasterForward => caster.transform.position + caster.transform.forward * areaRadius, AreaCenterType.GroundPoint => groundPosition ?? caster.transform.position, _ => caster.transform.position }; } #region Debug Visualization /// /// 공격 범위 시각화 (런타임 디버그용) /// public void DrawDebugRange(GameObject caster, float duration = 1f, Vector3? groundPosition = null) { if (targetType != TargetType.Area) return; Vector3 casterPos = caster.transform.position; Vector3 forward = caster.transform.forward; if (areaShape == AreaShapeType.Sphere) { Vector3 center = GetAreaCenter(caster, groundPosition); DebugDrawSphere(center, areaRadius, Color.red, duration); } else if (areaShape == AreaShapeType.Fan) { Vector3 center = GetAreaCenter(caster, groundPosition); DebugDrawFan(center, groundPosition.HasValue && areaCenter == AreaCenterType.GroundPoint ? (center - casterPos).normalized : 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, SingleAlly } public enum TargetTeam { Enemy, Ally, All } public enum AreaCenterType { Caster, CasterForward, GroundPoint // 지면 지점 타겟팅 (Ground Target) } public enum AreaShapeType { Sphere, // 원형 범위 Fan // 부채꼴 범위 } }