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

165 lines
9.3 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// PotionUse.cs — 전투 물약(런당 N개 · 최대 HP 비율 회복 · 쿨다운) — 813j 물약 버튼이 부르는 사용 API + 잔량/쿨 이벤트
//
// PD 지시 #813 · 발주서 WL-813i §1-2 · 기준서 v1 §B 요소 8 회복(3개/런 · 40 % · 쿨 5 s · Actor.Heal 사용) (2026-09-09)
//
// ■ 813j(UI) 가 쓰는 API
// · 사용 요청: PotionUse.TryUse() → PotionResult (Used 면 회복됨 · 그 외 거절 사유)
// · 상태 읽기: Remaining · Max · CooldownRemaining · CooldownTotal · IsReady (매 프레임 읽어도 GC 0)
// · 이벤트: PotionUse.Changed(in PotionEvent) — 잔량/쿨 변화 때마다(런 리셋 · 사용 · 쿨 종료 · 거절) · PotionUse.Used — 사용 성공만
// · 런 리셋: PotionUse.ResetRun() (813m 런 시작이 부를 수 있다 · potionResetOnMapLoad 면 맵 진입 때 자동)
// ■ 회복 = 원본 Actor.Heal(float rate, bool showhud) — 사망·만충이면 원본이 아무 것도 하지 않으므로 사용 전에 거절한다.
// ■ 시간 = 게임 시간(Time.time · 러너 Tick) — 일시정지·부활 대기(timeScale 0) 중엔 쿨이 흐르지 않는다.
// ■ 비활성(WLSurvivalSettings off)이면 TryUse 는 Disabled 를 돌려주고 이벤트 0 = 원본 100%(원본엔 전투 물약이 없다).
// ─────────────────────────────────────────────────────────────────────────────
using System;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Survival
{
public enum PotionResult { Used = 0, Disabled = 1, NoPC = 2, Dead = 3, Empty = 4, Cooldown = 5, FullHp = 6 }
public enum PotionChange { RunReset = 0, Used = 1, CooldownEnded = 2, Refused = 3 }
/// <summary>물약 상태 스냅샷(struct · GC 0). Changed/Used 구독자가 받는다.</summary>
public struct PotionEvent
{
public PCActor pc;
public PotionChange change;
public PotionResult result; // Refused 일 때 사유 · 그 외 Used
public int remaining, max;
public float cooldownRemaining, cooldownTotal;
public double healed, hpAfter, maxHp;
public float time;
public int frame;
}
public static class PotionUse
{
/// <summary>잔량/쿨 변화 통지(813j 버튼 갱신). 런 리셋 · 사용 · 쿨 종료 · 거절.</summary>
public static readonly CombatEventList<PotionEvent> Changed = new CombatEventList<PotionEvent>(4);
/// <summary>사용 성공 통지(연출 · 로그).</summary>
public static readonly CombatEventList<PotionEvent> Used = new CombatEventList<PotionEvent>(4);
static int s_remaining = -1; // -1 = 런 미시작(첫 조회 때 SO 값으로 채운다)
static float s_cooldownEnd = -1f;
static float s_now;
static bool s_cooldownNotified = true;
// ── 진단(프로브가 읽는다)
public static int UsedCount, RefusedCount, RunResets;
public static PotionResult LastResult;
public static double LastHealed;
static WLSurvivalSettings St { get { return WLSurvivalSettings.Instance; } }
/// <summary>남은 개수(런 미시작이면 SO 값).</summary>
public static int Remaining { get { EnsureRun(); return s_remaining; } }
/// <summary>런당 개수(SO).</summary>
public static int Max { get { var st = St; return st != null ? st.potionCountPerRun : 0; } }
/// <summary>쿨 남은 시간(초 · 게임 시간).</summary>
public static float CooldownRemaining { get { return Mathf.Max(0f, s_cooldownEnd - s_now); } }
/// <summary>쿨 전체 길이(SO · 813j 링 비율 = Remaining/Total).</summary>
public static float CooldownTotal { get { var st = St; return st != null ? st.potionCooldownSeconds : 0f; } }
/// <summary>지금 누르면 쓸 수 있는가(활성 · 잔량 · 쿨). HP 만충·사망은 TryUse 가 거절한다.</summary>
public static bool IsReady { get { return WLSurvivalSettings.Enabled && Remaining > 0 && CooldownRemaining <= 0f; } }
static void EnsureRun()
{
if (s_remaining >= 0) return;
var st = St;
s_remaining = st != null ? Mathf.Max(0, st.potionCountPerRun) : 0;
s_cooldownEnd = -1f; s_cooldownNotified = true;
}
/// <summary>메인 PC(MyValue.MyPC)에 사용.</summary>
public static PotionResult TryUse() { return TryUse(MyValue.MyPC); }
/// <summary>813j 물약 버튼이 부른다. Used 면 회복됨. 거절이어도 Changed(Refused · 사유) 를 내 UI 가 피드백할 수 있다.</summary>
public static PotionResult TryUse(PCActor pc)
{
if (!WLSurvivalSettings.Enabled) return Finish(PotionResult.Disabled, pc, 0d, false);
Survival.EnsureRunner();
EnsureRun();
if (pc == null) return Finish(PotionResult.NoPC, pc, 0d, true);
if (pc.IsDead()) return Finish(PotionResult.Dead, pc, 0d, true);
if (s_remaining <= 0) return Finish(PotionResult.Empty, pc, 0d, true);
if (s_now < s_cooldownEnd) return Finish(PotionResult.Cooldown, pc, 0d, true);
var st = St;
double before = pc.Get_HP(), max = pc.Get_MaxHP();
if (st.potionRefuseAtFullHp && before >= max) return Finish(PotionResult.FullHp, pc, 0d, true);
try { pc.Heal(st.potionHealRate, st.potionShowHealNumber); } // 원본 Heal(rate) · HUD 초록 숫자
catch (Exception ex) { Debug.LogException(ex); } // HUD 쪽 예외가 물약 상태를 깨지 않게 격리(회복 자체는 HP 갱신 뒤)
double healed = pc.Get_HP() - before;
s_remaining--;
s_cooldownEnd = s_now + Mathf.Max(0f, st.potionCooldownSeconds);
s_cooldownNotified = st.potionCooldownSeconds <= 0f;
UsedCount++; LastHealed = healed;
if (st.verboseLog)
Debug.Log("[PotionUse] used +" + healed.ToString("F0") + " hp=" + pc.Get_HP().ToString("F0") + "/" + max.ToString("F0")
+ " remaining=" + s_remaining + "/" + st.potionCountPerRun + " cd=" + st.potionCooldownSeconds.ToString("F1"));
return Finish(PotionResult.Used, pc, healed, true);
}
static PotionResult Finish(PotionResult r, PCActor pc, double healed, bool notify)
{
LastResult = r;
if (r != PotionResult.Used) RefusedCount++;
if (!notify) return r;
var e = Snapshot(pc, r == PotionResult.Used ? PotionChange.Used : PotionChange.Refused, r, healed);
Changed.Dispatch(in e);
if (r == PotionResult.Used) Used.Dispatch(in e);
return r;
}
static PotionEvent Snapshot(PCActor pc, PotionChange change, PotionResult r, double healed)
{
var st = St;
return new PotionEvent
{
pc = pc, change = change, result = r,
remaining = s_remaining < 0 ? 0 : s_remaining, max = st != null ? st.potionCountPerRun : 0,
cooldownRemaining = CooldownRemaining, cooldownTotal = st != null ? st.potionCooldownSeconds : 0f,
healed = healed,
hpAfter = pc != null ? pc.Get_HP() : 0d, maxHp = pc != null ? pc.Get_MaxHP() : 0d,
time = Time.unscaledTime, frame = Time.frameCount
};
}
/// <summary>런 시작(맵 진입 · 813m) — 잔량을 SO 값으로 · 쿨 해제. 비활성이면 이벤트 0.</summary>
public static void ResetRun(string reason = "")
{
var st = St;
s_remaining = st != null ? Mathf.Max(0, st.potionCountPerRun) : 0;
s_cooldownEnd = -1f; s_cooldownNotified = true;
RunResets++;
if (!WLSurvivalSettings.Enabled) return;
if (st.verboseLog) Debug.Log("[PotionUse] run reset (" + reason + ") remaining=" + s_remaining);
var e = Snapshot(MyValue.MyPC, PotionChange.RunReset, PotionResult.Used, 0d);
Changed.Dispatch(in e);
}
/// <summary>러너(또는 프로브)가 게임 시간을 넣는다. 쿨이 끝나는 프레임에 Changed(CooldownEnded) 1회.</summary>
public static void Tick(float gameNow)
{
s_now = gameNow;
if (s_cooldownNotified || s_now < s_cooldownEnd) return;
s_cooldownNotified = true;
if (!WLSurvivalSettings.Enabled) return;
var e = Snapshot(MyValue.MyPC, PotionChange.CooldownEnded, PotionResult.Used, 0d);
Changed.Dispatch(in e);
}
/// <summary>프로브용: 상태·카운터 초기화(구독은 유지).</summary>
public static void ResetDiagnostics()
{
s_remaining = -1; s_cooldownEnd = -1f; s_cooldownNotified = true;
UsedCount = RefusedCount = RunResets = 0; LastResult = PotionResult.Used; LastHealed = 0d;
Changed.Raised = Changed.Dispatched = 0; Used.Raised = Used.Dispatched = 0;
}
}
}