// ───────────────────────────────────────────────────────────────────────────── // 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 연결점 (「후속」 · 존재하지 않는 타입을 참조하지 않는다) ───── /// 남은 물약 개수. 813i 가 채운다. public static System.Func CountProvider; /// 남은 쿨(초). 813i 가 채운다. public static System.Func CooldownRemainProvider; /// 쿨 전체 길이(초). 813i 가 채운다. public static System.Func CooldownTotalProvider; /// 실제 사용. true = 소비 성공. 813i 가 채운다. public static System.Func UseHandler; /// 버튼을 눌렀다는 통지(로그·튜토리얼용 · 선택). public static System.Action UseRequested; /// 813i 가 연결됐는가(진단·완료보고 근거). 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(); } private void OnEnable() { Initialize(); } public string Initialize() { if (_rt == null) _rt = GetComponent(); 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(); } /// 잔량·쿨 링·흐림을 현재 값에 맞춘다(값이 그대로면 아무 것도 하지 않는다). 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; } /// 버튼 클릭. 813i Provider 가 없으면 아무 일도 하지 않는다(설정으로 완화 가능). 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(); bool made = false; if (group == null) { group = GetComponent(); if (group == null) { group = gameObject.AddComponent(); made = true; } } if (background == null) { background = GetComponent(); if (background == null) { background = gameObject.AddComponent(); made = true; } background.raycastTarget = true; } if (button == null) { button = GetComponent