380 lines
15 KiB
C#
380 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
/// <summary>
|
|
/// 보스 카운터 시스템 메인 컨트롤러.
|
|
/// (수정됨: Config나 Player가 없어도 에러가 나지 않도록 안전장치가 추가된 버전)
|
|
///
|
|
/// 핵심 흐름:
|
|
/// 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>();
|
|
|
|
// ═══════════════════════════════════════════
|
|
// 초기화
|
|
// ═══════════════════════════════════════════
|
|
|
|
private void Start()
|
|
{
|
|
// 🚨 [안전장치] 설정 파일이 없으면 경고 출력
|
|
if (config == null)
|
|
{
|
|
Debug.LogWarning("⚠ [BossCounterSystem] Config(설정 파일)가 없습니다! AI가 정상 작동하지 않을 수 있습니다.");
|
|
}
|
|
}
|
|
|
|
/// <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()
|
|
{
|
|
// 🚨 [안전장치] 플레이어 트래커나 설정 파일이 없으면 실행 중지 (NullReferenceException 방지)
|
|
if (PlayerBehaviorTracker.Instance == null || config == null) return;
|
|
|
|
EvaluateCounters();
|
|
DecayCounters();
|
|
CheckHabitChangeReward();
|
|
}
|
|
|
|
/// <summary>플레이어 행동 데이터를 읽고 카운터 모드 ON/OFF 판단</summary>
|
|
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} 카운터 활성화!");
|
|
}
|
|
}
|
|
|
|
/// <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 "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;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════
|
|
// 외부 조회 (여기가 아까 빠졌던 부분입니다!)
|
|
// ═══════════════════════════════════════════
|
|
|
|
/// <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;
|
|
} |