Projext/Assets/Scripts/Enemy/BossAI/BossCounterSystem.cs

357 lines
14 KiB
C#
Raw Normal View History

2026-02-10 15:29:22 +00:00
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 보스 카운터 시스템 메인 컨트롤러.
///
/// 핵심 흐름:
/// 1. PlayerBehaviorTracker에서 실시간 행동 데이터를 읽음
/// 2. 임계치 판단 → 카운터 모드 ON/OFF (잠금 해제 여부에 따라 임계치 다름)
/// 3. 가중치 기반으로 보스 패턴 선택
/// 4. 첫 발동 시 해당 카운터를 영구 잠금 해제
/// 5. 플레이어가 습관을 바꾸면 보상 (난이도 완화)
/// </summary>
public class BossCounterSystem : MonoBehaviour
{
[Header("참조")]
[SerializeField] private BossCounterConfig config;
[Header("이벤트 (연출/UI 연동)")]
[Tooltip("카운터 모드가 활성화될 때 발생 (CounterType 전달)")]
public UnityEvent<CounterType> OnCounterActivated;
[Tooltip("카운터 모드가 비활성화될 때 발생")]
public UnityEvent<CounterType> OnCounterDeactivated;
[Tooltip("보스가 카운터 패턴을 선택했을 때 발생 (패턴 이름 전달)")]
public UnityEvent<string> OnCounterPatternSelected;
[Tooltip("플레이어가 습관을 바꿔서 보상받을 때 발생")]
public UnityEvent OnHabitChangeRewarded;
// ── 카운터 모드 상태 ──
private Dictionary<CounterType, bool> activeCounters = new Dictionary<CounterType, bool>()
{
{ CounterType.Dodge, false },
{ CounterType.Aim, false },
{ CounterType.Pierce, false }
};
// ── Decay 타이머 ──
private Dictionary<CounterType, float> counterTimers = new Dictionary<CounterType, float>()
{
{ CounterType.Dodge, 0f },
{ CounterType.Aim, 0f },
{ CounterType.Pierce, 0f }
};
// ── 쿨타임 ──
private float lastCounterPatternTime = -100f;
// ── 습관 변경 보상 추적 ──
private HashSet<CounterType> previousRunCounters = new HashSet<CounterType>();
private HashSet<CounterType> currentRunActivatedCounters = new HashSet<CounterType>();
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<BossPattern> bossPatterns = new List<BossPattern>();
// ═══════════════════════════════════════════
// 초기화
// ═══════════════════════════════════════════
/// <summary>
/// 보스 전투 시작 시 호출. 이전 런에서 발동된 카운터 정보를 기반으로 초기화.
/// </summary>
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()
{
if (PlayerBehaviorTracker.Instance == null) return;
EvaluateCounters();
DecayCounters();
CheckHabitChangeReward();
}
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
private void EvaluateCounters()
{
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)
{
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} 카운터 활성화!");
}
}
/// <summary>시간이 지나면 카운터 모드 자동 해제</summary>
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로 비활성화");
}
}
}
// ═══════════════════════════════════════════
// 습관 변경 보상
// ═══════════════════════════════════════════
/// <summary>
/// 이전 런에서 카운터 당한 습관을 이번 런에서 보이지 않으면 보상.
/// 전투 중반(30초 이후) 한 번 체크.
/// </summary>
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에서 호출)
// ═══════════════════════════════════════════
/// <summary>
/// 현재 카운터 상태를 반영하여 보스 패턴을 가중치 랜덤으로 선택합니다.
/// 보스 AI의 패턴 선택 시점에 호출하세요.
/// </summary>
/// <returns>선택된 패턴 이름</returns>
public string SelectBossPattern()
{
if (bossPatterns.Count == 0)
{
Debug.LogWarning("[BossCounter] 보스 패턴이 등록되지 않았습니다!");
return "";
}
// 가중치 계산
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;
}
// ═══════════════════════════════════════════
// 외부 조회
// ═══════════════════════════════════════════
/// <summary>특정 카운터 모드가 현재 활성 상태인지 확인</summary>
public bool IsCounterActive(CounterType type)
{
return activeCounters.ContainsKey(type) && activeCounters[type];
}
/// <summary>현재 활성화된 모든 카운터 타입 반환</summary>
public List<CounterType> GetActiveCounters()
{
var result = new List<CounterType>();
foreach (var kvp in activeCounters)
{
if (kvp.Value) result.Add(kvp.Key);
}
return result;
}
/// <summary>습관 변경 보상이 활성화되었는지 확인</summary>
public bool IsHabitChangeRewarded => habitChangeRewardGranted;
}