201 lines
13 KiB
C#
201 lines
13 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
}
|