Project_WL/Assets/WL/Combat/Survival/AutoPotion.cs

201 lines
13 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.

// ─────────────────────────────────────────────────────────────────────────────
// AutoPotion.cs — 자동전투 중 HP 임계 이하가 되면 물약을 자동으로 쓴다 (Q5 D-21)
//
// PD 지시 #813 · 발주서 WL-813m2 §1-1 (2026-09-09)
// 근거 = qa/W2-5_재채점_v3.md **D-21**: 4분 런당 사망 **3회** · 결과 화면 「사망 / 물약 = 3 / 0」
// = 런당 3개의 물약을 **한 번도 쓰지 않고** 죽는다.
//
// ■ 왜 안 썼나 (실측 사슬)
// · 813x: 맵 진입 시 자동전투가 기본 ON 이 된다(`eCommon.Auto` · `PCActor.cs:185` 가 이 값으로 `m_Auto` 를 세우고
// `PCActor.cs:93` 자동 스킬 루프가 그 플래그로 게이트된다). 자동전투 런에서는 플레이어가 화면을 보고만 있다.
// · 813j/813y: 물약은 **버튼을 눌러야** 나간다(`PotionButton.UseHandler` → `PotionUse.TryUse()`).
// → 자동전투 + 버튼 입력 필요 = 물약 0개 사용. 813u 의 밸런스 산식(「물약 2~3개로 흡수」)이 전제부터 성립하지 않았다.
//
// ■ 이 파일이 하는 것 — 원본 훅 0 · UI 파일 0 · 폴링만(AutoCombatOnEnter · Survival.PollRunBoundary 선례)
// 메인 PC 의 HP 비율이 `autoPotionHpRatio`(SO · 기본 0.35) 이하가 되면 813i 공개 API `PotionUse.TryUse(pc)` 를 부른다.
// · 잔량·쿨은 813i 가 이미 관리한다 — 여기서는 `PotionUse.IsReady` 로 **먼저 걸러** 거절 이벤트를 매 프레임 쏘지 않는다(GC 0).
// · 자동전투가 꺼져 있으면(수동 전투) 쓰지 않는다 — `autoPotionRequireAutoCombat`(SO · 기본 on).
// 플레이어가 직접 조작 중이면 물약 타이밍도 플레이어 것이다.
// · 사망/부활 중(`DeathFlow.State != None`)에는 쓰지 않는다. 부활은 전회복이라(reviveHealRate=1) 쓸 이유도 없다.
// · 시간 축은 **게임 시간**(`Time.time`) — `holdWorldWhileReviving` 이 세운 timeScale=0 동안 폴링이 멈춘다.
//
// ■ 「자동 사용」 표시 — UI 파일 수정 0
// · 813y `PotionButton.Update()` 가 매 프레임 `CountProvider/CooldownRemainProvider` 를 다시 읽는다
// → 자동 사용이 **잔량 숫자 1 + 쿨 링**으로 즉시 보인다(연결은 813y `WLSurvivalUiBridge` 가 이미 해 놓았다).
// · `potionShowHealNumber=1` 이면 원본 `Actor.Heal(rate, showhud)` 이 HUDDMGUI 초록 회복 숫자를 띄운다.
// · 결과 화면 「물약」 칸도 자동으로 오른다 — `RunDirector.OnPotionUsed` 가 `PotionUse.Used` 를 구독한다(`RunDirector.cs:170`).
// · 별도 토스트는 만들지 않았다 — `LootToast` 에 외부에서 부를 수 있는 공개 표시 API 가 없어(실측) UI 파일을 고쳐야 한다.
// 그래서 이 파일은 `autoPotionLog` 로 Console 1줄만 남긴다.
// 🔴 `Debug.LogError/LogException` 을 쓰지 않는다 — `Util/ErrorLogHookManager.cs:33` 이 그것을 플레이어 모달로 띄운다(813x 교훈).
//
// ■ C8 롤백 = `WLSurvivalSettings.asset` 의 `autoPotionEnabled: 1 → 0`(코드 무수정) — 그러면 아래 Tick 은 첫 줄에서 돌아간다.
// 생존 축 전체 스위치(`enabled`)가 꺼져도 당연히 동작 0(`PotionUse` 자체가 Disabled).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(PCActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Survival
{
/// <summary>자동전투 중 HP 임계 이하면 813i 물약을 자동 사용. 정적 · 새 Manager/Singleton 0(틱은 SurvivalRunner 가 준다).</summary>
public static class AutoPotion
{
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
/// <summary>기능이 살아 있는가. 생존 축 스위치 + 자동 물약 스위치 + 런타임 차단 없음.</summary>
public static bool Active
{
get
{
if (RuntimeDisabled || !WLSurvivalSettings.Enabled) return false;
var st = St;
return st != null && st.autoPotionEnabled;
}
}
/// <summary>진단·A/B 전용 런타임 스위치(에셋 무수정 · Play 종료 시 도메인 리로드로 자동 false).</summary>
public static bool RuntimeDisabled;
/// <summary>
/// 프로브·QA 전용 자동전투 상태 강제값. 0 = 실제 값(`eCommon.Auto`) · 1 = 강제 ON · 1 = 강제 OFF.
/// 에디트 모드에는 `ServerInfo` 가 없어(만들면 에디트 모드를 오염시킨다) 실제 값을 읽을 수 없다.
/// </summary>
public static int AutoStateOverride;
/// <summary>지금 자동전투인가. 실제 값은 813x 의 가드된 읽기(`eCommon.Auto` · 플레이 모드에서만)를 재사용한다.</summary>
public static bool IsAutoCombatOn
{
get
{
if (AutoStateOverride != 0) return AutoStateOverride > 0;
return WL.Combat.Auto.AutoCombatOnEnter.IsAutoOn;
}
}
// ───────────────────────────────────────── 진단(프로브가 읽는다)
/// <summary>임계 아래로 내려가 사용을 시도한 횟수.</summary>
public static int Attempts;
/// <summary>실제로 자동 사용된 횟수(PotionResult.Used).</summary>
public static int Applied;
/// <summary>시도했는데 813i 가 거절한 횟수(만충·사망 등).</summary>
public static int Refusals;
/// <summary>자동전투 OFF 라 건너뛴 폴링 수.</summary>
public static int SkippedManual;
/// <summary>잔량 0 또는 쿨 중이라 건너뛴 폴링 수(= TryUse 를 부르지 않아 거절 이벤트도 없다).</summary>
public static int SkippedNotReady;
/// <summary>HP 가 임계 위라 건너뛴 폴링 수.</summary>
public static int SkippedHpOk;
/// <summary>사망·부활 중이라 건너뛴 폴링 수.</summary>
public static int SkippedDeath;
/// <summary>PC 가 없거나 MaxHP 가 0 이라 건너뛴 폴링 수.</summary>
public static int SkippedNoPc;
/// <summary>마지막 폴링에서 본 HP 비율(0~1 · 진단용).</summary>
public static float LastHpRatio;
/// <summary>마지막 TryUse 결과.</summary>
public static PotionResult LastResult;
/// <summary>마지막 자동 사용 시각(게임 시간).</summary>
public static float LastUseTime = -1f;
/// <summary>마지막 자동 사용 회복량(HP).</summary>
public static double LastHealed;
/// <summary>마지막 로그 문장(Console 이 꺼져 있어도 프로브가 읽는다).</summary>
public static string LastLog = "";
static float s_nextPoll;
static bool s_faulted;
static int s_faultCount;
const int FaultLimit = 5; // 연속 예외 5회면 이 세션 동안 중단(원본 동작으로 남는다)
// ─────────────────────────────────────────────────────────────────────
// 틱 — SurvivalRunner.Update() 가 PotionUse.Tick 뒤에 부른다(쿨 계산이 먼저 갱신돼야 한다)
// ─────────────────────────────────────────────────────────────────────
/// <summary>게임 시간(Time.time). 폴링 간격은 SO. 에디트 모드 프로브는 ForceTick 으로 직접 부른다.</summary>
internal static void Tick(float gameNow)
{
if (s_faulted || !Active) return;
var st = St;
if (gameNow < s_nextPoll) return;
s_nextPoll = gameNow + Mathf.Max(0.02f, st.autoPotionCheckSeconds);
// 🔴 예외를 삼키되 LogError 로 올리지 않는다(ErrorLogHookManager 모달 회피 · 813x 교훈).
try { Poll(st, gameNow); s_faultCount = 0; }
catch (System.Exception ex)
{
LastLog = "예외 — " + ex.Message;
if (++s_faultCount >= FaultLimit) { s_faulted = true; LastLog = "중단(연속 예외 " + s_faultCount + ") — " + ex.Message; }
}
}
static void Poll(WLSurvivalSettings st, float gameNow)
{
if (DeathFlow.State != DeathState.None) { SkippedDeath++; return; }
var pc = MyValue.MyPC; // 파괴된 참조는 Unity != 로 걸러진다
if (pc == null || pc.IsDead()) { SkippedNoPc++; return; }
if (st.autoPotionRequireAutoCombat && !IsAutoCombatOn) { SkippedManual++; return; }
// 잔량·쿨·전체 스위치는 813i 가 판정한다. 여기서 먼저 걸러야 매 프레임 Refused 이벤트가 나가지 않는다.
if (!PotionUse.IsReady) { SkippedNotReady++; return; }
double maxHp = pc.Get_MaxHP();
if (maxHp <= 0d) { SkippedNoPc++; return; }
float ratio = (float)(pc.Get_HP() / maxHp);
LastHpRatio = ratio;
if (ratio > st.autoPotionHpRatio) { SkippedHpOk++; return; }
Attempts++;
double before = pc.Get_HP();
var r = PotionUse.TryUse(pc);
LastResult = r;
if (r != PotionResult.Used) { Refusals++; return; }
Applied++;
LastUseTime = gameNow;
LastHealed = pc.Get_HP() - before;
if (st.autoPotionLog || st.verboseLog)
{
// 런당 최대 potionCountPerRun 회(기본 3) — 매 프레임 경로가 아니다.
LastLog = "자동 사용 #" + Applied + " — HP " + (ratio * 100f).ToString("F0") + "% ≤ "
+ (st.autoPotionHpRatio * 100f).ToString("F0") + "% · +" + LastHealed.ToString("F0")
+ " → " + pc.Get_HP().ToString("F0") + "/" + maxHp.ToString("F0")
+ " · 남은 물약 " + PotionUse.Remaining + "/" + PotionUse.Max;
Debug.Log("[AutoPotion] " + LastLog);
}
}
// ─────────────────────────────────────────────────────────────────────
// 진단 · 초기화
// ─────────────────────────────────────────────────────────────────────
/// <summary>프로브용: 러너 없이 1회 폴링(간격 무시).</summary>
public static void ForceTick(float gameNow) { s_nextPoll = float.MinValue; s_faulted = false; s_faultCount = 0; Tick(gameNow); }
/// <summary>프로브용: 카운터·폴링 상태 초기화.</summary>
public static void ResetDiagnostics()
{
Attempts = Applied = Refusals = 0;
SkippedManual = SkippedNotReady = SkippedHpOk = SkippedDeath = SkippedNoPc = 0;
LastHpRatio = 0f; LastResult = PotionResult.Used; LastUseTime = -1f; LastHealed = 0d; LastLog = "";
s_nextPoll = float.MinValue; s_faulted = false; s_faultCount = 0;
AutoStateOverride = 0; RuntimeDisabled = false;
}
/// <summary>진단 한 줄(프로브·QA 로그).</summary>
public static string Dump()
{
var st = St;
return "[AutoPotion] active=" + Active
+ " ratio<=" + (st != null ? st.autoPotionHpRatio.ToString("F2") : "?")
+ " requireAuto=" + (st != null && st.autoPotionRequireAutoCombat)
+ " autoOn=" + IsAutoCombatOn
+ " · attempts=" + Attempts + " applied=" + Applied + " refused=" + Refusals
+ " · skip(manual/notReady/hpOk/death/noPc)=" + SkippedManual + "/" + SkippedNotReady + "/"
+ SkippedHpOk + "/" + SkippedDeath + "/" + SkippedNoPc
+ " · lastHp=" + LastHpRatio.ToString("F2") + " last=" + LastResult
+ " potion=" + PotionUse.Remaining + "/" + PotionUse.Max;
}
}
}