- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP) - ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트 - 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom) - 씬, 프리팹, 애니메이션 등 전체 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
20 KiB
C#
275 lines
20 KiB
C#
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
|
|
using UnityEngine.Events; // 이벤트를 사용할거에요 -> UnityEngine.Events를
|
|
|
|
/// <summary>
|
|
/// 보스 카운터 시스템 (v2) — 반응형 행동, 패턴 반복 방지, 숨돌리기를 통합 관리
|
|
/// BossMonster.cs에서 이 시스템의 메서드를 호출하여 AI 판단을 위임합니다.
|
|
/// </summary>
|
|
public class BossCounterSystem : MonoBehaviour // 클래스를 선언할거에요 -> 보스 카운터 시스템을
|
|
{
|
|
// ══════════════════════════════════════════════════════════
|
|
// 기존 카운터 시스템 (하위 호환)
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
[Header("=== 기존 카운터 설정 ===")] // 인스펙터 헤더를 추가할거에요 -> 기존 카운터 설정을
|
|
[SerializeField] private BossCounterConfig config; // 변수를 선언할거에요 -> 설정 파일 에셋을
|
|
[SerializeField] private bool usePersistence = true; // 변수를 선언할거에요 -> 저장 사용 여부를
|
|
|
|
private Dictionary<CounterType, int> _counters = new Dictionary<CounterType, int>(); // 변수를 선언할거에요 -> 내부 카운터 데이터를
|
|
private PatternSelector _selector; // 변수를 선언할거에요 -> 패턴 선택 헬퍼를
|
|
private BossCounterPersistence _persistence; // 변수를 선언할거에요 -> 저장소 헬퍼를
|
|
|
|
public UnityEvent<CounterType, int> OnCounterUpdated; // 이벤트를 선언할거에요 -> 수치 변경 알림을
|
|
public UnityEvent<string> OnCounterActivated; // 이벤트를 선언할거에요 -> 활성화 알림을 (string)
|
|
public UnityEvent<string> OnCounterDeactivated; // 이벤트를 선언할거에요 -> 비활성화 알림을 (string)
|
|
public UnityEvent<string> 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<BossCounterPersistence>(); // 가져올거에요 -> 저장소 컴포넌트를
|
|
if (!_persistence) _persistence = gameObject.AddComponent<BossCounterPersistence>(); // 없으면 추가할거에요 -> 저장소를
|
|
|
|
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<CounterType>(_counters.Keys); // 복사할거에요 -> 키 목록을
|
|
foreach (var k in keys) if (_counters[k] > 0) _counters[k]--; // 줄일거에요 -> 쿨다운을 위해 값을
|
|
|
|
if (usePersistence && _persistence) _persistence.SaveData(_counters); // 저장할거에요 -> 갱신된 데이터를
|
|
return pattern; // 반환할거에요 -> 선택된 패턴 이름을
|
|
}
|
|
|
|
public Dictionary<CounterType, int> GetActiveCounters() => new Dictionary<CounterType, int>(_counters); // 반환할거에요 -> 데이터 복사본을
|
|
|
|
public bool IsHabitChangeRewarded() // 함수를 선언할거에요 -> 보상 획득 여부 확인을
|
|
{
|
|
return false; // 임시 반환할거에요 -> 로직 구현 전까지 거짓(false)을
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// SYSTEM 1 API: 반응형 행동
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 회피 반응이 발동되어야 하는지 판단합니다.
|
|
/// Recover 코루틴에서 호출해요.
|
|
/// </summary>
|
|
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; // 반환할거에요 -> 반응 필요함을
|
|
}
|
|
|
|
/// <summary>현재 회피 반응 중인지 여부</summary>
|
|
public bool IsDodgeReacting => _isDodgeReacting; // 속성을 반환할거에요 -> 회피 반응 여부를
|
|
|
|
/// <summary>회피 반응 시 와인드업 배율</summary>
|
|
public float DodgeReactWindupMult => dodgeReactWindupMult; // 속성을 반환할거에요 -> 와인드업 배율을
|
|
|
|
/// <summary>회피 반응 플래그를 해제합니다 (와인드업 적용 후 호출)</summary>
|
|
public void ClearDodgeReact() // 함수를 선언할거에요 -> 회피 반응 플래그를 해제하는
|
|
{
|
|
_isDodgeReacting = false; // 초기화할거에요 -> 반응 플래그를 (한 번만 사용하고 해제)
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어가 보스 뒤에 있는지 업데이트합니다.
|
|
/// BossMonster의 Update/TrackPlayerBehavior에서 매 프레임 호출해요.
|
|
/// </summary>
|
|
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; // 초기화할거에요 -> 뒤 아니면 리셋
|
|
}
|
|
|
|
/// <summary>뒤잡기 반응이 필요한지 여부</summary>
|
|
public bool ShouldReactToBehind() // 함수를 선언할거에요 -> 뒤잡기 반응 필요 여부를 판단하는
|
|
{
|
|
return _playerBehindTimer >= behindReactTime; // 반환할거에요 -> 뒤 있는 시간이 기준 초과했는지를
|
|
}
|
|
|
|
/// <summary>뒤잡기 타이머 리셋 (반응 후 호출)</summary>
|
|
public void ResetBehindTimer() // 함수를 선언할거에요 -> 뒤잡기 타이머를 초기화하는
|
|
{
|
|
_playerBehindTimer = 0f; // 초기화할거에요 -> 뒤잡기 타이머를 (반응 후)
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// SYSTEM 2 API: 패턴 히스토리 반복 방지
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 사용된 패턴을 히스토리에 기록합니다.
|
|
/// SelectAndFire에서 패턴 선택 후 호출해요.
|
|
/// </summary>
|
|
public void RecordPattern(int patternIndex) // 함수를 선언할거에요 -> 패턴을 히스토리에 기록하는
|
|
{
|
|
_patternHistory[_patternHistoryIndex] = patternIndex; // 저장할거에요 -> 현재 위치에 패턴을
|
|
_patternHistoryIndex = (_patternHistoryIndex + 1) % _patternHistory.Length; // 증가시킬거에요 -> 인덱스를 (순환)
|
|
if (_patternHistoryIndex == 0) _patternHistoryFilled = true; // 설정할거에요 -> 한 바퀴 도는 순간 플래그를
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 패턴의 히스토리 기반 가중치 감쇠 배율을 반환합니다.
|
|
/// 히스토리에 2회 이상 등장하면 0.4^(count-1) 식으로 감쇠.
|
|
/// </summary>
|
|
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)
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 공격 횟수를 증가시키고, 휴식이 필요한지 여부를 반환합니다.
|
|
/// Recover 코루틴 시작 시 호출해요.
|
|
/// </summary>
|
|
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; // 반환할거에요 -> 휴식 불필요함을
|
|
}
|
|
|
|
/// <summary>
|
|
/// 실제 휴식 시간(초)을 계산하여 반환합니다.
|
|
/// BossRest 코루틴에서 호출해요.
|
|
/// </summary>
|
|
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; // 반환할거에요 -> 계산된 휴식 시간을
|
|
}
|
|
|
|
/// <summary>공격 카운트를 0으로 리셋 (휴식 완료 후 호출)</summary>
|
|
public void ResetAttackCount() // 함수를 선언할거에요 -> 공격 카운트를 초기화하는
|
|
{
|
|
_attackCount = 0; // 초기화할거에요 -> 공격 카운트를 (휴식 후)
|
|
}
|
|
|
|
/// <summary>현재 공격 카운트 (디버그용)</summary>
|
|
public int AttackCount => _attackCount; // 속성을 반환할거에요 -> 현재 공격 카운트를
|
|
}
|