using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을 using UnityEngine.Events; // 이벤트를 사용할거에요 -> UnityEngine.Events를 /// /// 보스 카운터 시스템 (v2) — 반응형 행동, 패턴 반복 방지, 숨돌리기를 통합 관리 /// BossMonster.cs에서 이 시스템의 메서드를 호출하여 AI 판단을 위임합니다. /// public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> 보스 카운터 시스템을 { // ══════════════════════════════════════════════════════════ // 기존 카운터 시스템 (하위 호환) // ══════════════════════════════════════════════════════════ [Header("=== 기존 카운터 설정 ===")] // 인스펙터 헤더를 추가할거에요 -> 기존 카운터 설정을 [SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 설정 파일 에셋을 [SerializeField] private bool usePersistence = true; // 변수를 선언할거에요 -> 저장 사용 여부를 private Dictionary _counters = new Dictionary(); // 변수를 선언할거에요 -> 내부 카운터 데이터를 private PatternSelector _selector; // 변수를 선언할거에요 -> 패턴 선택 헬퍼를 private BossCounterPersistence _persistence; // 변수를 선언할거에요 -> 저장소 헬퍼를 public UnityEvent OnCounterUpdated; // 이벤트를 선언할거에요 -> 수치 변경 알림을 public UnityEvent OnCounterActivated; // 이벤트를 선언할거에요 -> 활성화 알림을 (string) public UnityEvent OnCounterDeactivated; // 이벤트를 선언할거에요 -> 비활성화 알림을 (string) public UnityEvent OnHabitChangeRewarded; // 이벤트를 선언할거에요 -> 보상 알림을 (string) // ══════════════════════════════════════════════════════════ // SYSTEM 1: 반응형 행동 (Reactive Behavior) // ══════════════════════════════════════════════════════════ [Header("=== 반응형 행동 ===")] // 인스펙터 헤더를 추가할거에요 -> 반응형 행동 섹션을 [Tooltip("플레이어가 최근에 회피했을 때 보스가 빠른 추가 공격을 할 확률 (0~1)\n추천: 0.3~0.5")] // 툴팁을 추가할거에요 -> 회피 반응 확률에 대한 설명을 [SerializeField] private float dodgeReactChance = 0.4f; // 변수를 선언할거에요 -> 회피 반응 확률을 [Tooltip("회피 반응 시 와인드업 시간 배율 (0.5 = 50% 단축)")] // 툴팁을 추가할거에요 -> 와인드업 배율에 대한 설명을 [SerializeField] private float dodgeReactWindupMult = 0.5f; // 변수를 선언할거에요 -> 반응 시 와인드업 배율을 [Tooltip("플레이어가 이 횟수 이상 회피하면 보스가 반응")] // 툴팁을 추가할거에요 -> 회피 감지 기준에 대한 설명을 [SerializeField] private int dodgeReactThreshold = 2; // 변수를 선언할거에요 -> 회피 감지 기준 횟수를 [Tooltip("플레이어가 보스 뒤에 있으면 반응하기까지의 시간 (초)")] // 툴팁을 추가할거에요 -> 뒤 감지 시간에 대한 설명을 [SerializeField] private float behindReactTime = 1.2f; // 변수를 선언할거에요 -> 뒤에 있으면 반응하는 시간을 private bool _isDodgeReacting = false; // 변수를 선언할거에요 -> 현재 회피 반응 중 여부를 private float _playerBehindTimer = 0f; // 변수를 선언할거에요 -> 플레이어가 보스 뒤에 있는 누적 시간을 // ══════════════════════════════════════════════════════════ // SYSTEM 2: 패턴 히스토리 반복 방지 // ══════════════════════════════════════════════════════════ [Header("=== 패턴 반복 방지 ===")] // 인스펙터 헤더를 추가할거에요 -> 패턴 반복 방지 섹션을 [Tooltip("히스토리 크기 (최근 몇 개 패턴을 기억할지)")] // 툴팁을 추가할거에요 -> 히스토리 크기에 대한 설명을 [SerializeField] private int historySize = 5; // 변수를 선언할거에요 -> 히스토리 크기를 (최근 몇 개 패턴) [Tooltip("히스토리 내 2회 이상 등장 시 가중치 감쇠 배율")] // 툴팁을 추가할거에요 -> 감쇠 배율에 대한 설명을 [SerializeField] private float historyPenalty = 0.4f; // 변수를 선언할거에요 -> 히스토리 감쇠 배율을 private int[] _patternHistory; // 변수를 선언할거에요 -> int 배열 (패턴 인덱스 기록)을 private int _patternHistoryIndex = 0; // 변수를 선언할거에요 -> 다음 기록할 인덱스를 private bool _patternHistoryFilled = false; // 변수를 선언할거에요 -> 5개 이상 기록됐는지 여부를 // ══════════════════════════════════════════════════════════ // SYSTEM 3: 숨돌리기 (Boss Rest / Breathing Room) // ══════════════════════════════════════════════════════════ [Header("=== 숨돌리기 (보스 휴식) ===")] // 인스펙터 헤더를 추가할거에요 -> 숨돌리기 섹션을 [Tooltip("연속 공격 횟수 한도\n추천: 3~5")] // 툴팁을 추가할거에요 -> 공격 예산에 대한 설명을 [SerializeField] private int attackBudget = 4; // 변수를 선언할거에요 -> 연속 공격 횟수 한도를 [Tooltip("휴식 시간 (초)\n추천: 2~4")] // 툴팁을 추가할거에요 -> 휴식 시간에 대한 설명을 [SerializeField] private float restDuration = 2.5f; // 변수를 선언할거에요 -> 휴식 시간을 (초) [Tooltip("Phase2 휴식 시간 배율")] // 툴팁을 추가할거에요 -> Phase2 배율에 대한 설명을 [SerializeField] private float phase2RestMult = 0.7f; // 변수를 선언할거에요 -> Phase2 휴식 시간 배율을 [Tooltip("Phase3 휴식 시간 배율")] // 툴팁을 추가할거에요 -> Phase3 배율에 대한 설명을 [SerializeField] private float phase3RestMult = 0.4f; // 변수를 선언할거에요 -> Phase3 휴식 시간 배율을 [Tooltip("Phase2 공격 예산 추가")] // 툴팁을 추가할거에요 -> Phase2 보너스에 대한 설명을 [SerializeField] private int phase2BudgetBonus = 1; // 변수를 선언할거에요 -> Phase2 공격 예산 추가를 [Tooltip("Phase3 공격 예산 추가")] // 툴팁을 추가할거에요 -> Phase3 보너스에 대한 설명을 [SerializeField] private int phase3BudgetBonus = 2; // 변수를 선언할거에요 -> Phase3 공격 예산 추가를 private int _attackCount = 0; // 변수를 선언할거에요 -> 현재 연속 공격 횟수를 // ══════════════════════════════════════════════════════════ // 초기화 // ══════════════════════════════════════════════════════════ private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를 { _selector = new PatternSelector(config); // 생성할거에요 -> 패턴 선택기를 _persistence = GetComponent(); // 가져올거에요 -> 저장소 컴포넌트를 if (!_persistence) _persistence = gameObject.AddComponent(); // 없으면 추가할거에요 -> 저장소를 foreach (CounterType t in System.Enum.GetValues(typeof(CounterType))) _counters[t] = 0; // 초기화할거에요 -> 모든 카운터를 0으로 _patternHistory = new int[historySize]; // 배열을 초기화할거에요 -> 패턴 히스토리를 } private void Start() // 함수를 실행할거에요 -> 시작 Start를 { if (usePersistence && _persistence) _persistence.LoadData(_counters); // 불러올거에요 -> 저장된 데이터를 } public void InitializeBattle() // 함수를 선언할거에요 -> 전투 시작 초기화를 { _attackCount = 0; // 초기화할거에요 -> 공격 카운트를 _isDodgeReacting = false; // 초기화할거에요 -> 회피 반응 플래그를 _playerBehindTimer = 0f; // 초기화할거에요 -> 뒤잡기 타이머를 _patternHistoryIndex = 0; // 초기화할거에요 -> 히스토리 인덱스를 _patternHistoryFilled = false; // 초기화할거에요 -> 히스토리 채워짐 플래그를 } // ══════════════════════════════════════════════════════════ // 기존 카운터 시스템 메서드 (하위 호환 유지) // ══════════════════════════════════════════════════════════ public void RegisterPlayerAction(CounterType type) // 함수를 선언할거에요 -> 플레이어 행동 기록을 { if (!_counters.ContainsKey(type)) _counters[type] = 0; // 없으면 만들거에요 -> 키를 _counters[type]++; // 증가시킬거에요 -> 카운트 값을 OnCounterUpdated?.Invoke(type, _counters[type]); // 알릴거에요 -> 수치 변경 이벤트를 OnCounterActivated?.Invoke(type.ToString()); // 알릴거에요 -> 활성화 이벤트를 문자열로 변환해서 } public string SelectBossPattern() // 함수를 선언할거에요 -> 패턴 선택을 { string pattern = _selector.SelectPattern(_counters); // 선택할거에요 -> 헬퍼를 통해 패턴을 var keys = new List(_counters.Keys); // 복사할거에요 -> 키 목록을 foreach (var k in keys) if (_counters[k] > 0) _counters[k]--; // 줄일거에요 -> 쿨다운을 위해 값을 if (usePersistence && _persistence) _persistence.SaveData(_counters); // 저장할거에요 -> 갱신된 데이터를 return pattern; // 반환할거에요 -> 선택된 패턴 이름을 } public Dictionary GetActiveCounters() => new Dictionary(_counters); // 반환할거에요 -> 데이터 복사본을 public bool IsHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 획득 여부 확인을 { return false; // 임시 반환할거에요 -> 로직 구현 전까지 거짓(false)을 } // ══════════════════════════════════════════════════════════ // SYSTEM 1 API: 반응형 행동 // ══════════════════════════════════════════════════════════ /// /// 회피 반응이 발동되어야 하는지 판단합니다. /// Recover 코루틴에서 호출해요. /// public bool ShouldDodgeReact() // 함수를 선언할거에요 -> 회피 반응 필요 여부를 판단하는 { if (PlayerBehaviorTracker.Instance == null) return false; // 중단할거에요 -> 추적기 없으면 if (PlayerBehaviorTracker.Instance.DodgeCount < dodgeReactThreshold) return false; // 중단할거에요 -> 회피 횟수 부족하면 if (Random.value >= dodgeReactChance) return false; // 중단할거에요 -> 확률 실패하면 _isDodgeReacting = true; // 설정할거에요 -> 반응 플래그를 (호출된 후 사용됨) return true; // 반환할거에요 -> 반응 필요함을 } /// 현재 회피 반응 중인지 여부 public bool IsDodgeReacting => _isDodgeReacting; // 속성을 반환할거에요 -> 회피 반응 여부를 /// 회피 반응 시 와인드업 배율 public float DodgeReactWindupMult => dodgeReactWindupMult; // 속성을 반환할거에요 -> 와인드업 배율을 /// 회피 반응 플래그를 해제합니다 (와인드업 적용 후 호출) public void ClearDodgeReact() // 함수를 선언할거에요 -> 회피 반응 플래그를 해제하는 { _isDodgeReacting = false; // 초기화할거에요 -> 반응 플래그를 (한 번만 사용하고 해제) } /// /// 플레이어가 보스 뒤에 있는지 업데이트합니다. /// BossMonster의 Update/TrackPlayerBehavior에서 매 프레임 호출해요. /// public void UpdateBehindTimer(Vector3 bossForward, Vector3 bossPos, Vector3 playerPos) // 함수를 선언할거에요 -> 뒤잡기 타이머를 업데이트하는 { Vector3 toPlayer = (playerPos - bossPos).normalized; // 계산할거에요 -> 보스에서 플레이어로의 방향을 float dot = Vector3.Dot(bossForward, toPlayer); // 계산할거에요 -> 정면 방향과의 내적을 (음수 = 뒤) if (dot < -0.3f) // 조건이 맞으면 실행할거에요 -> 플레이어가 보스 뒤에 있으면 _playerBehindTimer += Time.deltaTime; // 더할거에요 -> 뒤에 있는 시간을 else _playerBehindTimer = 0f; // 초기화할거에요 -> 뒤 아니면 리셋 } /// 뒤잡기 반응이 필요한지 여부 public bool ShouldReactToBehind() // 함수를 선언할거에요 -> 뒤잡기 반응 필요 여부를 판단하는 { return _playerBehindTimer >= behindReactTime; // 반환할거에요 -> 뒤 있는 시간이 기준 초과했는지를 } /// 뒤잡기 타이머 리셋 (반응 후 호출) public void ResetBehindTimer() // 함수를 선언할거에요 -> 뒤잡기 타이머를 초기화하는 { _playerBehindTimer = 0f; // 초기화할거에요 -> 뒤잡기 타이머를 (반응 후) } // ══════════════════════════════════════════════════════════ // SYSTEM 2 API: 패턴 히스토리 반복 방지 // ══════════════════════════════════════════════════════════ /// /// 사용된 패턴을 히스토리에 기록합니다. /// SelectAndFire에서 패턴 선택 후 호출해요. /// public void RecordPattern(int patternIndex) // 함수를 선언할거에요 -> 패턴을 히스토리에 기록하는 { _patternHistory[_patternHistoryIndex] = patternIndex; // 저장할거에요 -> 현재 위치에 패턴을 _patternHistoryIndex = (_patternHistoryIndex + 1) % _patternHistory.Length; // 증가시킬거에요 -> 인덱스를 (순환) if (_patternHistoryIndex == 0) _patternHistoryFilled = true; // 설정할거에요 -> 한 바퀴 도는 순간 플래그를 } /// /// 특정 패턴의 히스토리 기반 가중치 감쇠 배율을 반환합니다. /// 히스토리에 2회 이상 등장하면 0.4^(count-1) 식으로 감쇠. /// public float GetHistoryPenalty(int patternIndex) // 함수를 선언할거에요 -> 패턴의 히스토리 감쇠 배율을 계산하는 { int historyLen = _patternHistoryFilled ? _patternHistory.Length : _patternHistoryIndex; // 계산할거에요 -> 현재 히스토리 길이를 if (historyLen <= 0) return 1f; // 반환할거에요 -> 히스토리 없으면 페널티 없음을 int count = 0; // 초기화할거에요 -> 패턴 등장 횟수를 for (int i = 0; i < historyLen; i++) // 반복할거에요 -> 히스토리 길이만큼 { if (_patternHistory[i] == patternIndex) // 조건이 맞으면 실행할거에요 -> 패턴이 일치하면 count++; // 증가시킬거에요 -> 카운트를 } if (count >= 2) // 조건이 맞으면 실행할거에요 -> 2회 이상 등장했으면 return Mathf.Pow(historyPenalty, count - 1); // 반환할거에요 -> 지수 감쇠 값을 return 1f; // 반환할거에요 -> 1회 이하면 페널티 없음을 } // ══════════════════════════════════════════════════════════ // SYSTEM 3 API: 숨돌리기 (Attack Budget) // ══════════════════════════════════════════════════════════ /// /// 공격 횟수를 증가시키고, 휴식이 필요한지 여부를 반환합니다. /// Recover 코루틴 시작 시 호출해요. /// public bool RecordAttackAndCheckRest(bool isPhase2, bool isPhase3) // 함수를 선언할거에요 -> 공격을 기록하고 휴식 필요 여부를 확인하는 { _attackCount++; // 증가시킬거에요 -> 공격 카운트를 int currentBudget = attackBudget // 계산할거에요 -> 현재 Phase의 공격 예산을 + (isPhase3 ? phase3BudgetBonus : isPhase2 ? phase2BudgetBonus : 0); // 더할거에요 -> Phase별 보너스를 if (_attackCount >= currentBudget) // 조건이 맞으면 실행할거에요 -> 공격 예산 소진했으면 { Debug.Log($"[BossCounter] 공격 예산 소진 ({_attackCount}/{currentBudget}) → 숨돌리기!"); // 로그를 찍을거에요 return true; // 반환할거에요 -> 휴식 필요함을 } return false; // 반환할거에요 -> 휴식 불필요함을 } /// /// 실제 휴식 시간(초)을 계산하여 반환합니다. /// BossRest 코루틴에서 호출해요. /// public float GetRestDuration(bool isPhase2, bool isPhase3) // 함수를 선언할거에요 -> 실제 휴식 시간을 계산하는 { float actualRest = restDuration // 계산할거에요 -> 기본 휴식 시간에서 시작해서 * (isPhase3 ? phase3RestMult : isPhase2 ? phase2RestMult : 1f); // 곱할거에요 -> Phase별 배율을 actualRest += Random.Range(-0.3f, 0.3f); // 더할거에요 -> 약간의 랜덤을 (±0.3초) actualRest = Mathf.Max(actualRest, 0.5f); // 보장할거에요 -> 최소 0.5초를 (음수 방지) return actualRest; // 반환할거에요 -> 계산된 휴식 시간을 } /// 공격 카운트를 0으로 리셋 (휴식 완료 후 호출) public void ResetAttackCount() // 함수를 선언할거에요 -> 공격 카운트를 초기화하는 { _attackCount = 0; // 초기화할거에요 -> 공격 카운트를 (휴식 후) } /// 현재 공격 카운트 (디버그용) public int AttackCount => _attackCount; // 속성을 반환할거에요 -> 현재 공격 카운트를 }