Projext/Assets/Scripts/Player/Combat/Arrow.cs
2026-02-27 09:44:52 +09:00

459 lines
30 KiB
C#

using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
/// <summary>
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
/// [UPGRADED] 속성 데미지를 인스펙터에서 속성별로 개별 조정 가능
/// </summary>
public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
{
[Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을
[SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를
[SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를
[SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를
[SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을
[Header("--- 기본 화살 데미지 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 화살 데미지 --- 를
[Tooltip("기본 화살 데미지 (속성과 무관한 물리 데미지)")]
[SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지인 baseDamage를
[Header("--- 🔥 불 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 불 속성 섹션을
[SerializeField]
private ElementEffectData fireEffect = new ElementEffectData // 변수를 선언할거에요 -> 불 속성 데이터인 fireEffect를
{
bonusDamage = 5f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 5로
dotDamagePerTick = 3f, // 기본값을 설정할거에요 -> 틱당 화상 데미지를 3으로
dotDuration = 4f, // 기본값을 설정할거에요 -> 화상 지속시간을 4초로
dotTickInterval = 0.5f, // 기본값을 설정할거에요 -> 틱 간격을 0.5초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로 (불은 순수 데미지라 참고용)
};
[Header("--- ❄️ 얼음 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 얼음 속성 섹션을
[SerializeField]
private ElementEffectData iceEffect = new ElementEffectData // 변수를 선언할거에요 -> 얼음 속성 데이터인 iceEffect를
{
bonusDamage = 2f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 2로
dotDamagePerTick = 0f, // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (얼음은 슬로우 위주)
dotDuration = 3f, // 기본값을 설정할거에요 -> 슬로우 지속시간을 3초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 0.5f // 기본값을 설정할거에요 -> 슬로우 비율을 0.5 (50% 감속)로
};
[Header("--- ☠️ 독 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 독 속성 섹션을
[SerializeField]
private ElementEffectData poisonEffect = new ElementEffectData // 변수를 선언할거에요 -> 독 속성 데이터인 poisonEffect를
{
bonusDamage = 1f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 1로
dotDamagePerTick = 2f, // 기본값을 설정할거에요 -> 틱당 독 데미지를 2로
dotDuration = 6f, // 기본값을 설정할거에요 -> 독 지속시간을 6초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로
};
[Header("--- ⚡ 전기 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 전기 속성 섹션을
[SerializeField]
private ElementEffectData lightningEffect = new ElementEffectData // 변수를 선언할거에요 -> 전기 속성 데이터인 lightningEffect를
{
bonusDamage = 8f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 8로
dotDamagePerTick = 0f, // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (전기는 스턴 위주)
dotDuration = 1.5f, // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1.5f // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
};
[Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
[SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
// 화살 런타임 스탯 (Initialize에서 설정됨)
private float damage; // 변수를 선언할거에요 -> 최종 데미지 damage를
private float speed; // 변수를 선언할거에요 -> 속도 speed를
private float range; // 변수를 선언할거에요 -> 사거리 range를
private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를
private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을
private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로
private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로
// 현재 적용 중인 속성 (Initialize에서 설정됨)
private ArrowElementType currentElement = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 속성을 없음으로
// [NEW] 사운드 참조
private PlayerSoundFX _soundFX; // 변수를 선언할거에요 -> 사운드 스크립트 캐싱용을
// Raycast 추적용
private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를
private AudioSource _arrowAudioSource; // 변수를 선언할거에요 -> 화살 자체의 AudioSource를 (날아가는 소리용)
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 리지드바디를
_arrowAudioSource = GetComponent<AudioSource>(); // 가져올거에요 -> 화살 오브젝트의 AudioSource를 (날아가는 소리)
}
private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를
{
_soundFX = FindObjectOfType<PlayerSoundFX>(); // [NEW] 가져올거에요 -> 씬의 사운드 스크립트를 캐싱
if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면
{
Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을
if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을
arrowTip = tip ?? transform; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를
}
if (hitLayers.value == 0) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면
{
hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground"); // 값을 넣을거에요 -> 기본 충돌 레이어들을 (Default 제외)
}
}
/// <summary>
/// [UPGRADED] 초기화 — 속성 타입만 받고, 세부 수치는 인스펙터 설정값 사용
/// PlayerAttack.OnShootArrow()에서 호출됩니다.
/// </summary>
public void Initialize(
float dmg,
float arrowSpeed,
float maxRange,
Vector3 direction,
ArrowElementType element) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를 (5개 파라미터)
{
this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를
this.range = maxRange; // 값을 저장할거에요 -> 사거리를
this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을
this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를
this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로
this.currentElement = element; // 값을 저장할거에요 -> 현재 속성 타입을
// [UPGRADED] 기본 데미지 = 외부에서 받은 dmg + 속성 즉발 보너스 데미지
ElementEffectData effectData = GetElementData(element); // 데이터를 가져올거에요 -> 현재 속성에 맞는 설정값을
float bonusDmg = (effectData != null && effectData.enabled) ? effectData.bonusDamage : 0f; // 값을 계산할거에요 -> 속성 추가 즉발 데미지를
this.damage = dmg + bonusDmg; // 값을 저장할거에요 -> 기본 데미지 + 속성 보너스를 최종 데미지로
Debug.Log($"[Arrow] 초기화 — 기본:{dmg} + 속성보너스:{bonusDmg} = 최종:{this.damage} | 속성:{element}"); // 로그를 출력할거에요 -> 데미지 계산 내역을
// 발사 방향으로 회전
if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
{
transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록
}
// Rigidbody 설정
if (rb == null) rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{
rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로
rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로
rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로
}
Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로
}
/// <summary>
/// [하위 호환성] 기존 7-파라미터 버전도 유지 (외부에서 직접 수치를 넘기는 경우)
/// </summary>
public void Initialize(
float dmg,
float arrowSpeed,
float maxRange,
Vector3 direction,
ArrowElementType element,
float elemDmg,
float elemDur) // 함수를 선언할거에요 -> 구버전 호환용 7-파라미터 초기화 함수를
{
Initialize(dmg, arrowSpeed, maxRange, direction, element); // 실행할거에요 -> 새 5-파라미터 초기화를 (인스펙터 값 사용)
}
/// <summary>
/// [하위 호환성] 방향 없는 3-파라미터 버전
/// </summary>
public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 3-파라미터 초기화 함수를
{
Initialize(dmg, arrowSpeed, maxRange, transform.forward, ArrowElementType.None); // 실행할거에요 -> 신버전 초기화를 기본값으로
}
/// <summary>
/// [NEW] 속성 타입에 맞는 인스펙터 설정 데이터를 반환
/// </summary>
private ElementEffectData GetElementData(ArrowElementType element) // 함수를 선언할거에요 -> 속성 데이터를 가져오는 GetElementData를
{
switch (element) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: return fireEffect; // 반환할거에요 -> 불 속성 설정값을
case ArrowElementType.Ice: return iceEffect; // 반환할거에요 -> 얼음 속성 설정값을
case ArrowElementType.Poison: return poisonEffect; // 반환할거에요 -> 독 속성 설정값을
case ArrowElementType.Lightning: return lightningEffect; // 반환할거에요 -> 전기 속성 설정값을
default: return null; // 반환할거에요 -> 없음 (일반 화살)
}
}
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
{
if (!isFired || hasHit) return; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면
// 사거리 체크
float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를
if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면
{
Destroy(gameObject); // 파괴할거에요 -> 화살을
return; // 중단할거에요 -> 함수를
}
// 정밀 충돌 감지
CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를
previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로
}
/// <summary>
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
/// </summary>
private void CheckPrecisionCollision() // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을
{
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을
float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를
float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을
RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을
bool didHit = Physics.SphereCast(
tipPosition,
raycastRadius,
direction,
out hit,
checkDistance,
hitLayers
); // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을
if (showDebugRay) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면
{
Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로
}
if (didHit) // 조건이 맞으면 실행할거에요 -> 충돌했다면
{
HandleHit(hit.collider, hit.point); // 실행할거에요 -> 충돌 처리 함수 HandleHit을
}
}
/// <summary>
/// [UPGRADED] 충돌 처리 — 인스펙터 설정값 기반 속성 데미지 적용
/// </summary>
private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
{
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
// ⭐ 날아가는 소리 즉시 정지 — 충돌 순간 끊기지 않게
// 임팩트 소리("팍")는 _soundFX.PlayArrowHitFlesh()에서 별도 재생됨
if (_arrowAudioSource != null) _arrowAudioSource.Stop(); // 정지할거에요 -> 날아가는 소리를
// 적 감지 (Tag 또는 Layer)
bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
{
isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서
}
// [FIX] Enemy 레이어도 적으로 인식
int enemyLayerIndex = LayerMask.NameToLayer("Enemy"); // 값을 가져올거에요 -> Enemy 레이어 인덱스를
if (!isEnemy && enemyLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 아직 적으로 판별 안 됐고 Enemy 레이어가 유효하다면
{
isEnemy = hitCollider.gameObject.layer == enemyLayerIndex; // 조건을 갱신할거에요 -> Enemy 레이어인지 확인해서
}
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
{
// MonsterClass를 찾아 데미지 적용
MonsterClass monster = hitCollider.GetComponentInParent<MonsterClass>(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를
if (monster == null) monster = hitCollider.GetComponent<MonsterClass>(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를
// [FIX] MonsterClass가 없는 오브젝트(Sensor 등)는 무시 — 화살 통과
if (monster == null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트를 못 찾았다면
{
hasHit = false; // 되돌릴거에요 -> 충돌 상태를 해제해서 다음 충돌 가능하게
return; // 중단할거에요 -> 화살을 파괴하지 않고 통과시키기
}
// 1. 최종 데미지 적용 (기본 + 속성 보너스가 이미 합산됨)
monster.TakeDamage(damage); // 실행할거에요 -> 최종 데미지를 입히는 함수를
Debug.Log($"[Arrow] 적 명중! 최종 데미지: {damage} | 속성: {currentElement}"); // 로그를 출력할거에요 -> 명중 메시지를
// [NEW] 적 명중 사운드
if (_soundFX != null) _soundFX.PlayArrowHitFlesh(hitPoint); // 실행할거에요 -> 적 명중 소리를
// 2. [UPGRADED] 속성 지속 효과(DoT) 적용 — 인스펙터 설정값 사용
ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를
// 3. [NEW] 타격 임팩트 (히트스톱 + 카메라 흔들림 + 넉백)
bool isStrongHit = currentElement != ArrowElementType.None; // 판단할거에요 -> 속성 화살이면 강한 공격으로
if (HitImpactManager.Instance != null) // 조건이 맞으면 실행할거에요 -> 임팩트 매니저가 있다면
{
Vector3 hitDir = shootDirection != Vector3.zero ? shootDirection : transform.forward; // 방향을 결정할거에요 -> 화살 진행 방향을
HitImpactManager.Instance.TriggerHitImpact(monster, hitDir, hitPoint, isStrongHit); // 실행할거에요 -> 타격 임팩트를
}
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 잠시 뒤
}
else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
{
Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를
if (_soundFX != null) _soundFX.PlayArrowHitWall(hitPoint); // [NEW] 실행할거에요 -> 벽 명중 소리를
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
{
// [DEBUG] 뭐에 맞았는지 확인용 로그 — 문제 해결 후 삭제 가능
Debug.LogWarning($"[Arrow] 알 수 없는 충돌! 오브젝트: {hitCollider.gameObject.name}, 레이어: {LayerMask.LayerToName(hitCollider.gameObject.layer)}, 태그: {hitCollider.tag}, 위치: {hitPoint}"); // 경고를 출력할거에요 -> 충돌 대상 상세 정보를
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
}
/// <summary>
/// [UPGRADED] 속성 효과 적용 — 인스펙터에서 설정한 값을 직접 사용
/// MonsterClass에 ApplyStatusEffect(StatusEffectType, float dotDmg, float duration, float tickInterval, float strength)
/// 오버로드가 필요합니다.
/// </summary>
private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를
{
if (currentElement == ArrowElementType.None) return; // 조건이 맞으면 중단할거에요 -> 일반 화살이라면
ElementEffectData data = GetElementData(currentElement); // 데이터를 가져올거에요 -> 현재 속성의 인스펙터 설정값을
if (data == null || !data.enabled) return; // 조건이 맞으면 중단할거에요 -> 설정이 없거나 비활성이라면
switch (currentElement) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면
monster.ApplyStatusEffect( // 실행할거에요 -> 화상 상태이상을 적용하는 함수를
StatusEffectType.Burn, // 인자를 전달할거에요 -> 화상 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (인스펙터 값)
data.dotDuration, // 인자를 전달할거에요 -> 지속 시간을 (인스펙터 값)
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을 (인스펙터 값)
data.effectStrength // 인자를 전달할거에요 -> 효과 강도를 (인스펙터 값)
);
Debug.Log($"[Arrow] 🔥 화염! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)"); // 로그를 출력할거에요 -> 화상 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
monster.ApplyStatusEffect( // 실행할거에요 -> 슬로우 상태이상을 적용하는 함수를
StatusEffectType.Slow, // 인자를 전달할거에요 -> 슬로우 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (얼음은 보통 0)
data.dotDuration, // 인자를 전달할거에요 -> 슬로우 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 슬로우 비율을 (0.5면 50% 감속)
);
Debug.Log($"[Arrow] ❄️ 빙결! 슬로우:{data.effectStrength * 100}% x {data.dotDuration}초"); // 로그를 출력할거에요 -> 빙결 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면
monster.ApplyStatusEffect( // 실행할거에요 -> 독 상태이상을 적용하는 함수를
StatusEffectType.Poison, // 인자를 전달할거에요 -> 독 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 독 데미지를
data.dotDuration, // 인자를 전달할거에요 -> 독 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 효과 강도를
);
Debug.Log($"[Arrow] ☠️ 독! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)"); // 로그를 출력할거에요 -> 독 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 전기 속성이라면
monster.ApplyStatusEffect( // 실행할거에요 -> 감전(스턴) 상태이상을 적용하는 함수를
StatusEffectType.Shock, // 인자를 전달할거에요 -> 감전 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (전기는 보통 0)
data.dotDuration, // 인자를 전달할거에요 -> 스턴 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 스턴 시간을
);
Debug.Log($"[Arrow] ⚡ 감전! 스턴:{data.effectStrength}초 | 추가데미지:{data.bonusDamage}"); // 로그를 출력할거에요 -> 감전 효과 내역을
break; // 분기를 종료할거에요
}
}
/// <summary>
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
/// </summary>
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를
{
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면
bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
{
isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 레이어 확인으로
}
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
{
HandleHit(other, other.ClosestPoint(transform.position)); // 실행할거에요 -> 충돌 처리 함수를
}
else if (other.CompareTag("Wall") || other.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
{
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
}
/// <summary>
/// [PUBLIC] 외부에서 인스펙터 속성 데이터를 읽을 수 있는 접근자
/// UI 등에서 속성 정보를 표시할 때 사용
/// </summary>
public ElementEffectData GetCurrentElementData() // 함수를 선언할거에요 -> 현재 속성 데이터를 반환하는 접근자를
{
return GetElementData(currentElement); // 반환할거에요 -> 현재 속성의 설정 데이터를
}
/// <summary>
/// [PUBLIC] 현재 속성 타입 반환
/// </summary>
public ArrowElementType GetCurrentElement() // 함수를 선언할거에요 -> 현재 속성 타입을 반환하는 접근자를
{
return currentElement; // 반환할거에요 -> 현재 속성 타입을
}
/// <summary>
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
/// </summary>
private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
{
if (!Application.isPlaying || !isFired || !showDebugRay) return; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로
Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로
Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를
Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를
Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는
// 속성별 색상 표시
switch (currentElement) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로
case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로
}
if (currentElement != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
{
Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를
}
}
}