Project_WL/Assets/WL/UI/Scripts/PotionButton.cs

334 lines
17 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// PotionButton.cs — 전투 패드 물약 버튼(잔량 + 쿨 링) (WL-813tj · 813j · #813)
//
// 기준서 v1 §D-1 813j 행: "813c 의 빈 자리에 물약 버튼(잔량 · 쿨 링)"
// §B 요소 8: 물약 3개/런 · 40 % 회복 · 쿨 5 s.
//
// ■ 자리 (813c 소유)
// 813c 가 전투 패드 6슬롯 중 4를 스킬로 쓰고 **2를 예약**(물약 · 회피)으로 비활성화했다.
// 이 버튼은 그 예약 좌표(`WLHudLayoutSettings.ReserveSlotPx(index)`)에 **자기 노드**로 앉는다.
// 비활성 SkillCard 를 되살려 쓰지 않는 이유: `WLBattlePadLayout.Apply()` 가 매번 예약 슬롯을
// SetActive(false) 로 되돌리기 때문(813c 실측) — 켜 두면 두 코드가 서로 끈다.
//
// ■ 🔴 데이터는 813i(Gameplay) 가 주인 — 이 파일은 표시만 한다
// `WL.Combat.Survival.PotionUse` 는 **아직 없다**(브랜치 `wl/gameplay/WL-813i-survival` 은
// 2026-09-09 01:4x 기준 main 대비 커밋 0 · `Assets/WL/Combat/Survival/*.cs` 없음 = 실측).
// 존재하지 않는 타입을 참조하면 게이트가 깨지므로, **정적 델리게이트 자리**만 열어 둔다.
// 813i 는 자기 파일에서 아래 4개를 채우면 끝난다(UI 수정 0):
// PotionButton.CountProvider = () => PotionUse.Remain;
// PotionButton.CooldownRemainProvider = () => PotionUse.CooldownRemain;
// PotionButton.CooldownTotalProvider = () => PotionUse.CooldownTotal;
// PotionButton.UseHandler = () => PotionUse.TryUse();
// Provider 가 없으면 설정의 표시용 기본값(3개 · 5 s)을 그리고, 클릭은 무동작이다(오동작 방지).
//
// ■ 값 = WLSurvivalUiSettings.asset + 자리 = WLHudLayoutSettings.asset (둘 다 C45)
// 🔴 어셈블리 주의: Assets/WL/UI/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace WL.UI
{
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
public class PotionButton : MonoBehaviour
{
// ── 813i 연결점 (「후속」 · 존재하지 않는 타입을 참조하지 않는다) ─────
/// <summary>남은 물약 개수. 813i 가 채운다.</summary>
public static System.Func<int> CountProvider;
/// <summary>남은 쿨(초). 813i 가 채운다.</summary>
public static System.Func<float> CooldownRemainProvider;
/// <summary>쿨 전체 길이(초). 813i 가 채운다.</summary>
public static System.Func<float> CooldownTotalProvider;
/// <summary>실제 사용. true = 소비 성공. 813i 가 채운다.</summary>
public static System.Func<bool> UseHandler;
/// <summary>버튼을 눌렀다는 통지(로그·튜토리얼용 · 선택).</summary>
public static System.Action UseRequested;
/// <summary>813i 가 연결됐는가(진단·완료보고 근거).</summary>
public static bool HasProvider { get { return CountProvider != null || UseHandler != null; } }
[Header("구성 요소 (BuildIfNeeded 가 만든다)")]
[SerializeField] private Image background;
[SerializeField] private Image cooldownRing;
[SerializeField] private TextMeshProUGUI iconLabel;
[SerializeField] private TextMeshProUGUI countLabel;
[SerializeField] private Button button;
[SerializeField] private CanvasGroup group;
[Header("폰트 (에디터가 주입)")]
[SerializeField] private TMP_FontAsset font;
private RectTransform _rt;
private Canvas _canvas;
private int _lastCount = -1;
private float _lastFill = -1f;
// ── 진단 ──────────────────────────────────────────────────────────────
public int ClickCount { get; private set; }
public int UsedCount { get; private set; }
public int BlockedCount { get; private set; }
public int ShownCount { get { return _lastCount; } }
public float ShownFill { get { return _lastFill; } }
private void Awake() { _rt = GetComponent<RectTransform>(); }
private void OnEnable() { Initialize(); }
public string Initialize()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
BuildIfNeeded();
string layout = ApplyLayout();
Refresh();
return "initialized · " + layout + " · provider=" + HasProvider;
}
// ── 값 읽기 (813i 있으면 그쪽 · 없으면 표시용 기본값) ────────────────
public int ReadCount()
{
if (CountProvider != null) { try { return CountProvider(); } catch { } }
var s = WLSurvivalUiSettings.Instance;
return s != null ? s.potionFallbackCount : 0;
}
public float ReadCooldownFill()
{
var s = WLSurvivalUiSettings.Instance;
float total = 0f, remain = 0f;
if (CooldownTotalProvider != null) { try { total = CooldownTotalProvider(); } catch { } }
if (CooldownRemainProvider != null) { try { remain = CooldownRemainProvider(); } catch { } }
if (total <= 0f) total = s != null ? s.potionFallbackCooldownSeconds : 0f;
if (total <= 0f) return 0f;
return Mathf.Clamp01(remain / total);
}
private void Update() { Refresh(); }
/// <summary>잔량·쿨 링·흐림을 현재 값에 맞춘다(값이 그대로면 아무 것도 하지 않는다).</summary>
public string Refresh()
{
var s = WLSurvivalUiSettings.Instance;
if (s == null) return "WLSurvivalUiSettings 에셋 없음";
int count = ReadCount();
float fill = ReadCooldownFill();
bool ready = count > 0 && fill <= 0.0001f;
if (count != _lastCount)
{
_lastCount = count;
if (countLabel != null) countLabel.text = string.Format(s.potionCountFormat, count);
}
if (!Mathf.Approximately(fill, _lastFill))
{
_lastFill = fill;
if (cooldownRing != null)
{
cooldownRing.fillAmount = fill;
if (cooldownRing.enabled != (fill > 0.0001f)) cooldownRing.enabled = fill > 0.0001f;
}
}
var tint = ready ? s.potionReadyColor : s.potionDimColor;
if (iconLabel != null) iconLabel.color = tint;
if (countLabel != null) countLabel.color = tint;
if (group != null) group.alpha = ready ? 1f : 0.75f;
return "count=" + count + " fill=" + fill.ToString("F3") + " ready=" + ready;
}
/// <summary>버튼 클릭. 813i Provider 가 없으면 아무 일도 하지 않는다(설정으로 완화 가능).</summary>
public string OnClickPotion()
{
ClickCount++;
var s = WLSurvivalUiSettings.Instance;
if (s == null) return "설정 없음 — 무동작";
if (UseRequested != null) { try { UseRequested(); } catch { } }
if (UseHandler == null)
{
BlockedCount++;
if (s.verboseLog) Debug.Log("[PotionButton] 813i(PotionUse) 미연결 — 표시만 (후속)");
return s.potionRequireProvider ? "813i 미연결 — 무동작(후속)" : "813i 미연결 — 무동작";
}
bool ok = false;
try { ok = UseHandler(); } catch { ok = false; }
if (ok) UsedCount++; else BlockedCount++;
Refresh();
return ok ? "사용 성공" : "사용 불가(잔량 0 또는 쿨)";
}
// ── 구성 · 배치 ───────────────────────────────────────────────────────
public bool BuildIfNeeded()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
bool made = false;
if (group == null) { group = GetComponent<CanvasGroup>(); if (group == null) { group = gameObject.AddComponent<CanvasGroup>(); made = true; } }
if (background == null)
{
background = GetComponent<Image>();
if (background == null) { background = gameObject.AddComponent<Image>(); made = true; }
background.raycastTarget = true;
}
if (button == null)
{
button = GetComponent<Button>();
if (button == null) { button = gameObject.AddComponent<Button>(); made = true; }
button.targetGraphic = background;
button.onClick.RemoveListener(OnClickListener);
button.onClick.AddListener(OnClickListener);
}
if (cooldownRing == null)
{
var r = WLVignetteUtil.NewChild(_rt, "Ring");
WLVignetteUtil.Stretch(r);
cooldownRing = r.GetComponent<Image>();
if (cooldownRing == null) cooldownRing = r.gameObject.AddComponent<Image>();
cooldownRing.raycastTarget = false;
// 원본 SkillCard/Battle/i_cooltime 과 같은 규격(813c 실측: Filled · Radial360 · Top · 시계방향)
cooldownRing.type = Image.Type.Filled;
cooldownRing.fillMethod = Image.FillMethod.Radial360;
cooldownRing.fillOrigin = (int)Image.Origin360.Top;
cooldownRing.fillClockwise = true;
cooldownRing.fillAmount = 0f;
made = true;
}
if (iconLabel == null)
{
var t = WLVignetteUtil.NewChild(_rt, "Icon");
iconLabel = t.GetComponent<TextMeshProUGUI>();
if (iconLabel == null) iconLabel = t.gameObject.AddComponent<TextMeshProUGUI>();
iconLabel.raycastTarget = false;
iconLabel.alignment = TextAlignmentOptions.Center;
if (font != null) iconLabel.font = font;
made = true;
}
if (countLabel == null)
{
var t = WLVignetteUtil.NewChild(_rt, "Count");
countLabel = t.GetComponent<TextMeshProUGUI>();
if (countLabel == null) countLabel = t.gameObject.AddComponent<TextMeshProUGUI>();
countLabel.raycastTarget = false;
countLabel.alignment = TextAlignmentOptions.BottomRight;
if (font != null) countLabel.font = font;
made = true;
}
return made;
}
private void OnClickListener() { OnClickPotion(); }
/// <summary>813c 예약 슬롯 좌표에 버튼을 앉힌다(자리·크기의 주인은 WLHudLayoutSettings).</summary>
public string ApplyLayout()
{
if (_rt == null) _rt = GetComponent<RectTransform>();
var s = WLSurvivalUiSettings.Instance;
var hud = WLHudLayoutSettings.Instance;
if (s == null) return "WLSurvivalUiSettings 에셋 없음 — 배치 건너뜀";
if (hud == null) return "WLHudLayoutSettings 에셋 없음 — 배치 건너뜀";
bool on = s.potionButtonEnabled;
if (gameObject.activeSelf != on) gameObject.SetActive(on);
if (!on) return "potionButtonEnabled=false — 숨김";
float u = UnitsPerPx();
Vector2 px = hud.ReserveSlotPx(Mathf.Max(0, s.potionReserveSlotIndex));
WLBattlePadLayout.PlaceFromCorner(_rt, px, hud.reserveDiameterPx, u, hud.mirrorLeftHanded);
if (background != null) background.color = s.potionBgColor;
if (cooldownRing != null) cooldownRing.color = s.potionCooldownColor;
if (iconLabel != null)
{
var t = iconLabel.rectTransform;
WLVignetteUtil.Stretch(t);
iconLabel.fontSize = s.potionLabelFontPx * u;
iconLabel.text = s.potionLabelText;
if (font != null && iconLabel.font != font) iconLabel.font = font;
}
if (countLabel != null)
{
var t = countLabel.rectTransform;
WLVignetteUtil.Stretch(t);
t.offsetMin = new Vector2(0f, 0f);
t.offsetMax = new Vector2(-4f * u, -4f * u);
countLabel.fontSize = s.potionCountFontPx * u;
if (font != null && countLabel.font != font) countLabel.font = font;
}
_lastCount = -1; _lastFill = -1f; // 다음 Refresh 가 강제로 다시 그리게
return "배치 dx/dy px=" + px + " 지름=" + hud.reserveDiameterPx + "px pos" + _rt.anchoredPosition +
" size" + _rt.sizeDelta + " 앵커" + _rt.anchorMin + " unitsPerPx=" + u.ToString("F4");
}
public float UnitsPerPx()
{
if (_canvas == null) _canvas = GetComponentInParent<Canvas>(true);
return WLVignetteUtil.UnitsPerPx(_canvas);
}
public void SetFont(TMP_FontAsset f)
{
font = f;
if (f == null) return;
if (iconLabel != null) iconLabel.font = f;
if (countLabel != null) countLabel.font = f;
}
// ── 검증 전용 ────────────────────────────────────────────────────────
/// <summary>813i 대역(가짜 Provider) — 잔량·쿨·사용을 UI 만으로 왕복 검증한다.</summary>
public static string InstallFakeProvider(int count, float cooldownTotal)
{
int remain = count;
float cd = 0f;
CountProvider = () => remain;
CooldownTotalProvider = () => cooldownTotal;
CooldownRemainProvider = () => cd;
UseHandler = () =>
{
if (remain <= 0 || cd > 0f) return false;
remain--; cd = cooldownTotal; return true;
};
FakeCooldownSetter = v => cd = Mathf.Max(0f, v);
return "가짜 Provider 설치 count=" + count + " cd=" + cooldownTotal + "s (813i 대역)";
}
/// <summary>가짜 Provider 의 남은 쿨을 직접 흘린다(프로브가 시간을 대신 준다).</summary>
public static System.Action<float> FakeCooldownSetter;
public static string ClearProvider()
{
CountProvider = null; CooldownRemainProvider = null; CooldownTotalProvider = null;
UseHandler = null; UseRequested = null; FakeCooldownSetter = null;
return "Provider 해제(813i 미연결 상태로 복귀)";
}
public string Dump()
{
var sb = new StringBuilder();
var s = WLSurvivalUiSettings.Instance;
var hud = WLHudLayoutSettings.Instance;
sb.AppendLine("[PotionButton] provider=" + HasProvider + " clicks=" + ClickCount +
" used=" + UsedCount + " blocked=" + BlockedCount +
" 표시잔량=" + _lastCount + " 링fill=" + _lastFill.ToString("F3"));
if (_rt != null)
sb.AppendLine(" rect pos" + _rt.anchoredPosition + " size" + _rt.sizeDelta +
" aMin" + _rt.anchorMin + " aMax" + _rt.anchorMax + " scale" + _rt.localScale +
" active=" + gameObject.activeSelf);
if (cooldownRing != null)
sb.AppendLine(" ring type=" + cooldownRing.type + " method=" + cooldownRing.fillMethod +
" origin=" + cooldownRing.fillOrigin + " cw=" + cooldownRing.fillClockwise +
" fill=" + cooldownRing.fillAmount.ToString("F3") + " enabled=" + cooldownRing.enabled);
sb.AppendLine(" 라벨 icon=\"" + (iconLabel != null ? iconLabel.text : "-") + "\" count=\"" +
(countLabel != null ? countLabel.text : "-") + "\" 버튼=" + (button != null));
if (hud != null && s != null)
sb.AppendLine(" 자리 = 813c ReserveSlotPx(" + s.potionReserveSlotIndex + ")=" + hud.ReserveSlotPx(s.potionReserveSlotIndex) +
" 지름=" + hud.reserveDiameterPx + "px mirror=" + hud.mirrorLeftHanded);
sb.AppendLine(" 설정 " + (s == null ? "(에셋 없음)" :
"enabled=" + s.potionButtonEnabled + " fallback(count=" + s.potionFallbackCount +
" cd=" + s.potionFallbackCooldownSeconds + "s) requireProvider=" + s.potionRequireProvider));
return sb.ToString();
}
}
}