Projext/Assets/02_Scripts/Enemy/BossAI/NorcielBoss.PatternSelection.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

258 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
// ============================================================
// NorcielBoss.PatternSelection.cs — 패턴 선택 AI 로직 (partial class)
//
// [역할]
// 거리/상황 기반 가중치 패턴 선택 시스템
// - closeFactor, farFactor, midFactor 계산
// - 플레이어 행동 감지 기반 가중치 보정
// - 카운터 시스템 히스토리 감쇠 적용
// - 가중치 룰렛으로 최종 패턴 결정
// ============================================================
public partial class NorcielBoss : MonsterClass
{
private void SelectAndFire(float dist) // 함수를 선언할거에요 -> 가중치 기반 적응형 패턴을 선택하고 시작하는
{
// ══════════════════════════════════════════════════════
// 가중치(Weight) 기반 패턴 선택 시스템
//
// 모든 패턴에 기본 가중치를 부여하고, 상황에 따라 가중치를 조정해요.
// - 거리 조건에 안 맞는 패턴은 가중치 0 (선택 불가)
// - 직전에 쓴 패턴은 가중치 대폭 감소 (연속 방지)
// - 플레이어 행동에 따라 특정 패턴 가중치 증가 (적응형)
// - 최종 가중치 기반 확률 룰렛으로 패턴 결정
// ══════════════════════════════════════════════════════
// ── 쇠공 상태 확인 ───────────────────────────────
bool ballIsHeld = ironBall != null && ironBall.State == BossIronBall.BallState.Held; // 확인할거에요 -> 쇠공 들고 있는지를
// ── 각 패턴별 기본 가중치 설정 ──────────────────
float wThrow = 0f; // 초기화할거에요 -> Throw 가중치를
float wSmash = 0f; // 초기화할거에요 -> Smash 가중치를
float wSweep = 0f; // 초기화할거에요 -> Sweep 가중치를
float wDash = 0f; // 초기화할거에요 -> Dash 가중치를
float wDashSmash = 0f; // 초기화할거에요 -> DashSmash 가중치를
bool isKiting = _kitingTimer >= kitingDetectTime; // 판단할거에요 -> 카이팅 여부를 (kitingDetectTime초 이상 거리 유지 + 피격)
// ══════════════════════════════════════════════════════
// [개선 v2] 거리 기반 부드러운 가중치 시스템
//
// 핵심 원리:
// - closeFactor, farFactor, midFactor 세 가지 비율로 패턴 가중치를 보간
// - "AI 판단 거리"는 meleeRange 기준 (smashRadius는 데미지 판정 범위이지 AI 판단 기준이 아님)
// - 어느 거리에서든 모든 패턴이 확률적으로 섞이되, 거리에 따라 비중이 달라짐
//
// [v2 수정] smashRadius가 아무리 커도 패턴 선택에 영향 안 줌
// → effectiveMeleeMax를 meleeRange + 슬라이드 접근 거리로 계산
// → smashRadius는 "맞는 범위"일 뿐, "패턴을 고르는 거리"가 아님
// ══════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// [v3 수정] 실제 타격 범위 기반 가중치 시스템
//
// 핵심 변경: effectiveMeleeMax를 "실제 Smash가 닿는 거리"로 수정
// 기존 문제: slideApproachDist(3m) 때문에 8m까지 Smash 가중치가 남아있었음
// → 6~8m에서 Smash를 골라놓고 실제로는 안 닿는 "데드존" 발생
// 수정: Smash 실제 타격 범위(smashForwardOffset + smashRadius) + 여유 1m로 제한
// ══════════════════════════════════════════════════════════
// Smash가 실제로 닿는 최대 거리 (판정 중심 오프셋 + 판정 반경 + 약간의 이동 여유)
float actualSmashRange = smashForwardOffset + smashRadius + 1f; // 계산할거에요 -> Smash 실제 타격 최대 거리를 (1.5+3+1=5.5m)
// AI가 "근접"이라고 판단하는 최대 거리
// ⚠️ 기존 slideApproachDist(3m) 제거 → Smash가 안 닿는 거리에서 선택되는 버그 방지
float effectiveMeleeMax = actualSmashRange; // 설정할거에요 -> AI 근접 판단 최대 거리를 (실제 타격 범위 기준)
// closeFactor: 0(먼 거리) ~ 1(근접) — actualSmashRange 이내에서만 높음
float closeFactor = Mathf.Clamp01(1f - (dist / effectiveMeleeMax)); // 계산할거에요 -> 근접 비율을 (가까울수록 1, 5.5m 밖이면 0)
// farFactor: 0(근접) ~ 1(먼 거리) — meleeRange부터 throwRange까지 선형 증가
float farFactor = Mathf.Clamp01((dist - meleeRange) / Mathf.Max(throwRange - meleeRange, 1f)); // 계산할거에요 -> 원거리 비율을 (멀수록 1)
// midFactor: 중간 거리(meleeRange~throwRange 사이)에서 가장 높음 — 삼각형 분포
float midPoint = (meleeRange + throwRange) * 0.5f; // 계산할거에요 -> 중간 거리를 (예: 6.75m)
float midHalfWidth = (throwRange - meleeRange) * 0.5f; // 계산할거에요 -> 삼각형 분포 반폭을
float midFactor = 1f - Mathf.Abs(dist - midPoint) / Mathf.Max(midHalfWidth, 1f); // 계산할거에요 -> 중간 거리 비율을 (중간일수록 1)
midFactor = Mathf.Clamp01(midFactor); // 보장할거에요 -> 0~1 범위를
// ── Smash: 실제 타격 범위 안에서만 가중치 부여 ──────────
// [v3] actualSmashRange(5.5m) 밖이면 완전 차단 → 빗나갈 Smash 원천 차단
wSmash = 25f * closeFactor; // 계산할거에요 -> 거리 보간된 Smash 가중치를 (가까우면 25, 멀면 0에 수렴)
if (dist > actualSmashRange) wSmash = 0f; // 차단할거에요 -> Smash가 실제로 닿을 수 없으면 (5.5m 초과)
// ── Sweep: 근접 전용, Smash보다 좁은 범위 ─────────
wSweep = sweepSelectChance * 50f * closeFactor; // 계산할거에요 -> 거리 보간된 Sweep 가중치를
if (dist > sweepRadius + 2f) wSweep = 0f; // 차단할거에요 -> 스윕 범위 밖이면
// ── Throw: 중~원거리에서 주력, 근접에서도 서프라이즈 ──
if (ballIsHeld) // 조건이 맞으면 실행할거에요 -> 쇠공 들고 있으면
{
float throwBase = isKiting ? 30f : 40f; // 설정할거에요 -> Throw 기본치를 (카이팅 시 약간 줄임)
// 가까울 때도 최소 20% 확보 → 근접에서 가끔 던지기 = 의외성
wThrow = throwBase * (farFactor * 0.4f + midFactor * 0.4f + 0.2f); // 계산할거에요 -> 거리 보간된 Throw 가중치를
}
else // 쇠공 없을 때
{
wThrow = 0f; // 설정할거에요 -> 쇠공 없으면 Throw 불가
}
// ── Dash: 중~원거리 갭클로저, 근접에서는 낮음 ──
// [v5 수정] 기존 문제: 바닥 40% + 쇠공 없음 ×2.0 → 돌진 편향
// 수정: 바닥 20%로 낮추고, 쇠공 없음 보정도 완화
float dashBase = isKiting ? 15f : 10f; // 설정할거에요 -> Dash 기본치를 (v5: 18/12 → 15/10 하향)
wDash = dashBase * (farFactor * 0.8f + 0.2f); // 계산할거에요 -> 거리 보간된 Dash 가중치를 (v5: 바닥 20%, 멀면 높고 가까우면 매우 낮음)
if (!ballIsHeld && dist > meleeRange) wDash *= 1.5f; // 올릴거에요 -> 쇠공 없이 원거리면 갭클로저 보정 (v5: 2.0 → 1.5 완화)
// ── DashSmash: 중~원거리 갭클로저 콤보, 근접에선 가중치 낮음 ──
// [v5 수정] 기존 문제: 바닥 배율 0.5(50%) → 근접에서도 항상 높은 가중치 → DashSmash 편향
// 수정: 바닥 배율 0.15(15%)로 낮추고, 기본값도 Phase별 하향 → 근접 패턴과 균형
float dsmashPhaseBase = _isPhase3 ? 15f : _isPhase2 ? 12f : 6f; // 설정할거에요 -> Phase별 DashSmash 기본치를 (v5: 25/18/10 → 15/12/6 하향)
wDashSmash = dsmashPhaseBase * (farFactor * 0.85f + 0.15f); // 계산할거에요 -> 거리 보간된 DashSmash 가중치를 (v5: 바닥 15%로 하향, 먼 거리에서만 높음)
if (isKiting) wDashSmash *= 1.5f; // 올릴거에요 -> 카이팅 시 콤보 대시 보정 (v5: 2.0 → 1.5 완화)
if (!ballIsHeld && dist > throwRange) wDashSmash *= 1.3f; // 올릴거에요 -> 쇠공 없이 멀면 돌진 보정 (v5: 1.8 → 1.3 완화)
// ── 카운터 시스템 패턴 보정 ─────────────────────
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템 있으면
{
string counterResult = counterSystem.SelectBossPattern(); // 가져올거에요 -> 카운터 결과를
if (!string.IsNullOrEmpty(counterResult) && counterResult != "default" && counterResult != "Normal") // 조건이 맞으면 실행할거에요 -> 특정 패턴 지정이면
{
// 카운터가 특정 패턴을 권장하면 해당 가중치를 1.5배 보정 (강제가 아닌 유도)
if (counterResult.Contains("Smash")) wSmash *= 1.5f; // 올릴거에요 -> Smash 보정을
else if (counterResult.Contains("Throw")) wThrow *= 1.5f; // 올릴거에요 -> Throw 보정을
else if (counterResult.Contains("Dash")) wDash *= 1.5f; // 올릴거에요 -> Dash 보정을
else if (counterResult.Contains("Sweep")) wSweep *= 1.5f; // 올릴거에요 -> Sweep 보정을
}
}
// ── [적응형 AI] 플레이어 행동에 따른 가중치 보정 ──
//
// 1) 플레이어가 정지해 있으면 → 갭클로저(Dash/DashSmash) 가중치 상승
// "서서 힐 쓰거나 가만히 있으면 벌 준다"
if (_playerStillTimer >= stillPunishTime) // 조건이 맞으면 실행할거에요 -> 오래 멈춰있으면
{
wDash *= 2.0f; // 올릴거에요 -> 대시 가중치를 (가만히 있으면 돌진)
wDashSmash *= 2.5f; // 올릴거에요 -> 콤보 대시를 (강력한 징벌)
wThrow *= 0.3f; // 낮출거에요 -> 던지기를 (멀리서 던질 이유 없음)
}
// 2) 플레이어가 접근 중이면 → Smash/Sweep 가중치 상승
// "다가오는 플레이어를 근접기로 맞이한다"
if (_playerApproachTimer >= approachReactTime) // 조건이 맞으면 실행할거에요 -> 접근 감지되면
{
wSmash *= 1.8f; // 올릴거에요 -> 스매시를 (다가오는 걸 내려찍기)
wSweep *= 1.5f; // 올릴거에요 -> 스윕을 (범위 공격으로 견제)
wDash *= 0.3f; // 낮출거에요 -> 대시를 (다가오는데 돌진할 이유 없음)
}
// 3) 플레이어가 도주 중이면 → 갭클로저 가중치 상승 (과하지 않게)
// "도망가면 쫓아가서 잡는다"
// [v5 수정] 누적 곱셈 폭발 방지 → 배율 완화
if (_playerFleeTimer >= 1.0f) // 조건이 맞으면 실행할거에요 -> 도주 감지되면
{
wDash *= 1.8f; // 올릴거에요 -> 대시를 (추격, v5: 2.5 → 1.8)
wDashSmash *= 2.0f; // 올릴거에요 -> 콤보 대시를 (v5: 3.0 → 2.0)
wSmash *= 0.3f; // 낮출거에요 -> 근접기를 (닿지도 못하니)
}
// 4) 카이팅 감지 (활 쏘면서 거리 유지) → 갭클로저 우선 (과하지 않게)
// "원거리 공격하면서 도망다니면 쫓아간다"
// [v5 수정] 기존 문제: ×3.0, ×4.0 곱셈 → 도주+카이팅 중복 시 DashSmash가 전체의 90%
// 수정: 배율 완화 + Throw도 약간 남겨서 패턴 다양성 확보
if (isKiting) // 조건이 맞으면 실행할거에요 -> 카이팅 감지되면
{
wDash *= 2.0f; // 올릴거에요 -> 대시를 (빠르게 붙기, v5: 3.0 → 2.0)
wDashSmash *= 2.5f; // 올릴거에요 -> 콤보 대시를 (v5: 4.0 → 2.5)
wThrow *= 0.3f; // 낮출거에요 -> 던지기를 (v5: 0.2 → 0.3, 약간 남김)
wSmash *= 0.1f; // 낮출거에요 -> 근접기를 (닿을 수가 없으니)
wSweep *= 0.1f; // 낮출거에요 -> 근접기를 (닿을 수가 없으니)
}
// ── [v3 추가] 5) 중거리 캠핑 감지 → 갭클로저 + 던지기 강화 ──
// "멀리 서서 때리고만 있으면 무조건 쫓아가거나 던진다"
// 조건: 플레이어가 정지 + Smash 타격 범위 밖 + 사거리 안
// 기존 문제: 보스가 중거리에서 Smash만 반복 → 전부 빗나감
// 해결: Smash/Sweep 가중치를 강제로 0으로, 갭클로저를 크게 올림
bool isMidRangeCamping = _playerStillTimer >= stillPunishTime // 조건 1: 플레이어가 오래 정지 중
&& dist > actualSmashRange // 조건 2: Smash가 닿지 않는 거리
&& dist <= throwRange; // 조건 3: 사거리 안 (throwRange 이내)
if (isMidRangeCamping) // 조건이 맞으면 실행할거에요 -> 중거리 캠핑 감지되면
{
wSmash = 0f; // 강제할거에요 -> Smash를 0으로 (어차피 안 닿으니)
wSweep = 0f; // 강제할거에요 -> Sweep도 0으로 (근접기 전부 차단)
wDash *= 2.5f; // 올릴거에요 -> 대시를 (거리를 좁혀서 근접)
wDashSmash *= 3.0f; // 올릴거에요 -> 콤보 대시를 (돌진 후 내려찍기)
if (ballIsHeld) wThrow *= 2.5f; // 올릴거에요 -> 던지기를 (가만히 있으면 던져서 맞춤)
Debug.Log($"[Boss] 중거리 캠핑 감지! dist={dist:F1}m > Smash범위{actualSmashRange:F1}m → 갭클로저/던지기 강화"); // 로그를 찍을거에요
}
// ── [연속 방지] 직전 패턴 가중치 감소 ─────────────
// 같은 패턴이 2회 이상 연속되면 가중치를 대폭 줄여요.
// 완전히 0으로 만들지는 않아요 (가끔 의도적 반복 = 읽기 어려움).
if (_samePatternCount >= 1) // 조건이 맞으면 실행할거에요 -> 같은 패턴이 최소 1회 나왔으면
{
float penalty = _samePatternCount >= 2 ? 0.05f : 0.25f; // 계산할거에요 -> 감쇠 배율을 (2연속이면 95% 감소, 1연속이면 75% 감소)
switch (_lastPattern) // 분기할거에요 -> 직전 패턴에 따라
{
case BossPattern.Throw: wThrow *= penalty; break; // 감소시킬거에요 -> Throw 가중치를
case BossPattern.Smash: wSmash *= penalty; break; // 감소시킬거에요 -> Smash 가중치를
case BossPattern.Sweep: wSweep *= penalty; break; // 감소시킬거에요 -> Sweep 가중치를
case BossPattern.Dash: wDash *= penalty; break; // 감소시킬거에요 -> Dash 가중치를
case BossPattern.DashSmash: wDashSmash *= penalty; break; // 감소시킬거에요 -> DashSmash 가중치를
}
}
// ── [히스토리 기반] 최근 패턴 등장 빈도 감쇠 → counterSystem에 위임 ──
if (counterSystem != null) // 조건이 맞으면 실행할거에요 -> 카운터 시스템 있으면
{
wThrow *= counterSystem.GetHistoryPenalty(0); // 적용할거에요 -> Throw(0) 히스토리 감쇠를
wSmash *= counterSystem.GetHistoryPenalty(1); // 적용할거에요 -> Smash(1) 히스토리 감쇠를
wSweep *= counterSystem.GetHistoryPenalty(2); // 적용할거에요 -> Sweep(2) 히스토리 감쇠를
wDash *= counterSystem.GetHistoryPenalty(3); // 적용할거에요 -> Dash(3) 히스토리 감쇠를
wDashSmash *= counterSystem.GetHistoryPenalty(4); // 적용할거에요 -> DashSmash(4) 히스토리 감쇠를
}
// ── 가중치 룰렛 (Weighted Random) ────────────────
float total = wThrow + wSmash + wSweep + wDash + wDashSmash; // 합산할거에요 -> 전체 가중치를
if (total <= 0f) total = 1f; // 방어할거에요 -> 0 나누기 방지
float roll = Random.value * total; // 굴릴거에요 -> 0~total 사이 랜덤을
BossPattern pattern; // 선언할거에요 -> 최종 선택 패턴을
if (roll < wThrow) // 판단할거에요 -> Throw 구간이면
pattern = BossPattern.Throw;
else if (roll < wThrow + wSmash) // 판단할거에요 -> Smash 구간이면
pattern = BossPattern.Smash;
else if (roll < wThrow + wSmash + wSweep) // 판단할거에요 -> Sweep 구간이면
pattern = BossPattern.Sweep;
else if (roll < wThrow + wSmash + wSweep + wDash) // 판단할거에요 -> Dash 구간이면
pattern = BossPattern.Dash;
else // 나머지 = DashSmash
pattern = BossPattern.DashSmash;
// ── 연속 카운터 갱신 ────────────────────────────
if (pattern == _lastPattern) // 조건이 맞으면 실행할거에요 -> 같은 패턴이 또 나왔으면
_samePatternCount++; // 증가시킬거에요 -> 연속 카운터를
else
_samePatternCount = 0; // 초기화할거에요 -> 다른 패턴이니 리셋
_lastPattern = pattern; // 기록할거에요 -> 이번 패턴을 (다음 비교용)
if (counterSystem != null) counterSystem.RecordPattern((int)pattern); // 기록할거에요 -> 선택된 패턴을 카운터 시스템 히스토리에
// [v6 추가] SelectAndFire에서 Dash/DashSmash 선택 시 전역 대시 쿨다운 설정
// v5 문제: FSM 트리거(Chase/PreAttack)에서만 쿨다운 설정 → SelectAndFire에서 선택된 대시는 쿨다운 미적용
// → 쿨타임 만료 후 SelectAndFire에서 Dash 선택 → 즉시 Chase에서 또 대시 발동 가능
// v6 수정: SelectAndFire에서도 대시 선택 시 _dashCooldownTimer 설정 → 전역 쿨다운 일관성
if (pattern == BossPattern.Dash || pattern == BossPattern.DashSmash) // 조건이 맞으면 실행할거에요 -> 대시 계열 패턴이면
_dashCooldownTimer = dashCooldownTime; // 설정할거에요 -> 대시 쿨다운을 (다음 대시까지 6초 대기)
Debug.Log($"[Boss] 패턴 선택: {pattern} dist={dist:F1}m Phase={(_isPhase3 ? "3" : _isPhase2 ? "2" : "1")} 콤보#{_comboCounter} " +
$"가중치=[T:{wThrow:F0} S:{wSmash:F0} Sw:{wSweep:F0} D:{wDash:F0} DS:{wDashSmash:F0}] " +
$"정지={_playerStillTimer:F1}s 접근={_playerApproachTimer:F1}s 도주={_playerFleeTimer:F1}s"); // 로그를 찍을거에요
StartPattern(pattern); // 실행할거에요 -> 선택된 패턴을
}
}