using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; /// /// 보스 카운터 시스템 메인 컨트롤러. /// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전) /// /// 핵심 흐름: /// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음 /// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름) /// 3. 가중치 기반으로 보스 패턴 선택 /// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제 /// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화) /// public class BossCounterSystem : MonoBehaviour { [Header("참조")] [SerializeField] private BossCounterConfig config; [Header("이벤트 (연출/UI 연동)")] [Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")] public UnityEvent OnCounterActivated; [Tooltip("카운터 모드가 비활성화될 때 발생")] public UnityEvent OnCounterDeactivated; [Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")] public UnityEvent OnCounterPatternSelected; [Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")] public UnityEvent OnHabitChangeRewarded; // ── 카운터 모드 상태 ── private Dictionary activeCounters = new Dictionary() { { CounterType.Dodge, false }, { CounterType.Aim, false }, { CounterType.Pierce, false } }; // ── Decay 타이머 ── private Dictionary counterTimers = new Dictionary() { { CounterType.Dodge, 0f }, { CounterType.Aim, 0f }, { CounterType.Pierce, 0f } }; // ── 쿨타임 ── private float lastCounterPatternTime = -100f; // ── 습관 변경 보상 추적 ── private HashSet previousRunCounters = new HashSet(); private HashSet currentRunActivatedCounters = new HashSet(); private bool habitChangeRewardGranted = false; // ── 보스 패턴 가중치 정의 ── [System.Serializable] public class BossPattern { public string patternName; public float baseWeight = 1f; [Tooltip("이 패턴이 카운터 패턴인 경우 해당 타입")] public CounterType counterType = CounterType.None; [Tooltip("카운터 발동 시 보조적으로 가중치가 올라가는 타입")] public CounterType subCounterType = CounterType.None; } [Header("보스 패턴 목록")] [SerializeField] private List bossPatterns = new List(); // ═══════════════════════════════════════════ // 초기화 // ═══════════════════════════════════════════ private void Start() { // 🚨 [안전장치] 설정 파일이 없으면 경고 출력 if (config == null) { Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다."); } } /// /// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화. /// public void InitializeBattle() { // 이전 런에서 잠금 해제된 카운터들을 기록 (습관 변경 보상 판단용) previousRunCounters.Clear(); currentRunActivatedCounters.Clear(); habitChangeRewardGranted = false; var persistence = BossCounterPersistence.Instance; if (persistence != null) { if (persistence.Data.dodgeCounterActivations > 0 && persistence.IsUnlocked(CounterType.Dodge)) previousRunCounters.Add(CounterType.Dodge); if (persistence.Data.aimCounterActivations > 0 && persistence.IsUnlocked(CounterType.Aim)) previousRunCounters.Add(CounterType.Aim); if (persistence.Data.pierceCounterActivations > 0 && persistence.IsUnlocked(CounterType.Pierce)) previousRunCounters.Add(CounterType.Pierce); } // 모든 카운터 모드 OFF foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) { activeCounters[type] = false; counterTimers[type] = 0f; } lastCounterPatternTime = -100f; Debug.Log($"[BossCounter] 전투 시작. 이전 런 카운터: [{string.Join(", ", previousRunCounters)}]"); } // ═══════════════════════════════════════════ // 매 프레임 업데이트 // ═══════════════════════════════════════════ private void Update() { // 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지) if (PlayerBehaviorTracker.Instance == null || config == null) return; EvaluateCounters(); DecayCounters(); CheckHabitChangeReward(); } /// 플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단 private void EvaluateCounters() { // 🚨 [안전장치] Config가 없으면 계산 불가 if (config == null) return; var tracker = PlayerBehaviorTracker.Instance; var persistence = BossCounterPersistence.Instance; // ── 회피 카운터 ── bool dodgeUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Dodge); int dodgeThreshold = config.GetEffectiveDodgeThreshold(dodgeUnlocked); if (tracker.DodgeCount >= dodgeThreshold) { // 잠금 해제 안 된 상태면 첫 발동 시 잠금 해제 (첫 런에서도 발동 가능) // 잠금 해제된 상태면 더 낮은 임계치로 발동 ActivateCounter(CounterType.Dodge); } // ── 조준 카운터 ── bool aimUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Aim); float aimThreshold = config.GetEffectiveAimThreshold(aimUnlocked); if (tracker.AimHoldTime >= aimThreshold) { ActivateCounter(CounterType.Aim); } // ── 관통 카운터 ── bool pierceUnlocked = persistence != null && persistence.IsUnlocked(CounterType.Pierce); float pierceThreshold = config.GetEffectivePierceThreshold(pierceUnlocked); if (tracker.TotalShotsInWindow >= config.minShotsForPierceCheck && tracker.PierceRatio >= pierceThreshold) { ActivateCounter(CounterType.Pierce); } } private void ActivateCounter(CounterType type) { // 🚨 [안전장치] Config 확인 if (config == null) return; counterTimers[type] = config.counterDecayTime; if (!activeCounters[type]) { activeCounters[type] = true; currentRunActivatedCounters.Add(type); // 영구 잠금 해제 var persistence = BossCounterPersistence.Instance; if (persistence != null) { persistence.UnlockCounter(type); persistence.RecordActivation(type); } OnCounterActivated?.Invoke(type); Debug.Log($"[BossCounter] {type} 카운터 활성화!"); } } /// 시간이 지나면 카운터 모드 자동 해제 private void DecayCounters() { foreach (var type in new[] { CounterType.Dodge, CounterType.Aim, CounterType.Pierce }) { if (!activeCounters[type]) continue; counterTimers[type] -= Time.deltaTime; if (counterTimers[type] <= 0f) { activeCounters[type] = false; OnCounterDeactivated?.Invoke(type); Debug.Log($"[BossCounter] {type} 카운터 Decay로 비활성화"); } } } // ═══════════════════════════════════════════ // 습관 변경 보상 // ═══════════════════════════════════════════ /// /// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상. /// 전투 중반(30초 이후) 한 번 체크. /// private float battleTimer = 0f; private const float HABIT_CHECK_TIME = 30f; private void CheckHabitChangeReward() { if (habitChangeRewardGranted || previousRunCounters.Count == 0) return; battleTimer += Time.deltaTime; if (battleTimer < HABIT_CHECK_TIME) return; // 이전 런에서 발동된 카운터 중, 이번 런에서 아직 발동 안 된 것이 있으면 보상 foreach (var prevCounter in previousRunCounters) { if (!currentRunActivatedCounters.Contains(prevCounter)) { habitChangeRewardGranted = true; OnHabitChangeRewarded?.Invoke(); Debug.Log($"[BossCounter] 플레이어가 {prevCounter} 습관을 바꿈! 보상 부여"); break; } } } // ═══════════════════════════════════════════ // 패턴 선택 (보스 AI에서 호출) // ═══════════════════════════════════════════ /// /// 현재 카운터 상태를 반영하여 보스 패턴을 가중치 랜덤으로 선택합니다. /// 보스 AI의 패턴 선택 시점에 호출하세요. /// /// 선택된 패턴 이름 public string SelectBossPattern() { if (bossPatterns.Count == 0) { Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!"); return "Normal"; // 기본값 리턴 } // 🚨 [안전장치] Config가 없으면 그냥 랜덤 패턴 반환 (멈춤 방지) if (config == null) { int randomIndex = Random.Range(0, bossPatterns.Count); return bossPatterns[randomIndex].patternName; } // 가중치 계산 float[] weights = new float[bossPatterns.Count]; float totalWeight = 0f; float counterWeight = 0f; for (int i = 0; i < bossPatterns.Count; i++) { weights[i] = bossPatterns[i].baseWeight; // 카운터 패턴 가중치 증가 if (bossPatterns[i].counterType != CounterType.None && activeCounters.ContainsKey(bossPatterns[i].counterType) && activeCounters[bossPatterns[i].counterType]) { // 쿨타임 체크 if (Time.time - lastCounterPatternTime >= config.counterCooldown) { weights[i] += config.counterWeightBonus; } } // 보조 카운터 가중치 if (bossPatterns[i].subCounterType != CounterType.None && activeCounters.ContainsKey(bossPatterns[i].subCounterType) && activeCounters[bossPatterns[i].subCounterType]) { weights[i] += config.counterSubWeightBonus; } // 습관 변경 보상: 일반 패턴 가중치 감소 (= 난이도 완화) if (habitChangeRewardGranted && bossPatterns[i].counterType == CounterType.None) { weights[i] *= (1f - config.habitChangeRewardRatio); } totalWeight += weights[i]; if (bossPatterns[i].counterType != CounterType.None) counterWeight += weights[i]; } // 빈도 상한 체크: 카운터 패턴 총 비율이 maxCounterFrequency를 넘지 않도록 if (totalWeight > 0f && counterWeight / totalWeight > config.maxCounterFrequency) { float allowedCounterWeight = (totalWeight - counterWeight) * config.maxCounterFrequency / (1f - config.maxCounterFrequency); float scale = allowedCounterWeight / counterWeight; for (int i = 0; i < bossPatterns.Count; i++) { if (bossPatterns[i].counterType != CounterType.None) { float bonus = weights[i] - bossPatterns[i].baseWeight; weights[i] = bossPatterns[i].baseWeight + bonus * scale; } } // 총 가중치 재계산 totalWeight = 0f; for (int i = 0; i < weights.Length; i++) totalWeight += weights[i]; } // 가중치 랜덤 선택 float roll = Random.Range(0f, totalWeight); float cumulative = 0f; for (int i = 0; i < bossPatterns.Count; i++) { cumulative += weights[i]; if (roll <= cumulative) { string selected = bossPatterns[i].patternName; // 카운터 패턴이면 쿨타임 기록 if (bossPatterns[i].counterType != CounterType.None) { lastCounterPatternTime = Time.time; } OnCounterPatternSelected?.Invoke(selected); return selected; } } return bossPatterns[bossPatterns.Count - 1].patternName; } // ═══════════════════════════════════════════ // 외부 조회 (여기가 아까 빠졌던 부분입니다!) // ═══════════════════════════════════════════ /// 특정 카운터 모드가 현재 활성 상태인지 확인 public bool IsCounterActive(CounterType type) { return activeCounters.ContainsKey(type) && activeCounters[type]; } /// 현재 활성화된 모든 카운터 타입 반환 public List GetActiveCounters() { var result = new List(); foreach (var kvp in activeCounters) { if (kvp.Value) result.Add(kvp.Key); } return result; } /// 습관 변경 보상이 활성화되었는지 확인 public bool IsHabitChangeRewarded => habitChangeRewardGranted; }