// ───────────────────────────────────────────────────────────────────────────── // WLBossToken.cs — 보스 처치 재화 「보스 증표」 + 땅 확장 가격판 재화 훅 (#825 · 2026-10-09) // // PD 2026-10-09: 「보스를 잡으면 땅이 열리는 게 아니라, 보스를 잡아서 얻는 재화로 열릴 수 있는 형태면 충분」 // - 지급: 던전 보스 스테이지 클리어(StageClearResult.isBoss && win) → WLIslandSettings.bossTokenPerBossClear 개 // (PlayerPrefs 저장 · 세이브 초기화(WLIslandStart.Prepare) 때 0) // - 소비: FI 확장 가격판(Purchaser)이 WLLandCurrency 를 통해 잔액을 읽고 깎는다 // (expansionCurrency 1 = 증표 · 0 = 코인 원본 그대로) — Purchaser.cs 훅 4줄(🔴 WL 훅 (#825) 표시) // - 가격: expansionBossTokenPrices[열린 땅 수 − expansionBaseUnlockedCount] (표 밖은 마지막 값) // - 던전 해금 「골드 납품 발판」(WLGoldPlatform)은 같은 Purchaser 를 재사용하므로 코인 그대로(부모 감지) // - 표시: WLHudBinder 의 특수 재화 칸(specialItemId 0 이면) + 가격판 글자 색·「증표」 접두(WLBossPricePad) // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; using WL.Combat.Stage; using FIIslandManager = CryingSnow.FarmingIsland.IslandManager; using FIPurchaser = CryingSnow.FarmingIsland.Purchaser; namespace WL.Island { public static class WLBossToken { public static event System.Action Changed; public static int Granted, Spent; public static string LastLog = ""; static string Key(WLIslandSettings cfg) { return cfg != null && !string.IsNullOrEmpty(cfg.bossTokenPrefsKey) ? cfg.bossTokenPrefsKey : "WL_Island_BossToken"; } public static string Name { get { var cfg = WLIslandSettings.Instance; return cfg != null && !string.IsNullOrEmpty(cfg.bossTokenName) ? cfg.bossTokenName : "보스 증표"; } } public static int Get() { return PlayerPrefs.GetInt(Key(WLIslandSettings.Instance), 0); } public static void Set(int v) { PlayerPrefs.SetInt(Key(WLIslandSettings.Instance), Mathf.Max(0, v)); PlayerPrefs.Save(); if (Changed != null) Changed(); } public static void Add(int n) { if (n <= 0) return; Set(Get() + n); Granted += n; } public static bool TrySpend(int n) { if (n <= 0) return true; int cur = Get(); if (cur < n) return false; Set(cur - n); Spent += n; return true; } public static void Reset() { Set(0); LastLog = Name + " 0 (세이브 초기화)"; } /// 던전 스테이지 클리어 훅(WLIslandBridge.OnStageCleared) — 보스 스테이지 승리만 센다. public static void OnStageCleared(in StageClearResult r, WLIslandSettings cfg) { if (cfg == null || !r.win || !r.isBoss) return; int n = cfg.bossTokenPerBossClear; if (n <= 0) return; Add(n); LastLog = "보스 처치 — " + Name + " +" + n + " (보유 " + Get() + ")"; WLIslandBridge.Log(cfg, LastLog); } } /// FI Purchaser 훅 — 가격판이 쓰는 재화(코인 / 보스 증표)와 가격. public static class WLLandCurrency { static readonly HashSet s_bossPads = new HashSet(); // 증표 모드로 바뀐 Purchaser(instanceID) public static int PadsConverted; public static string LastLog = ""; static float s_lastShortLogAt = -99f; public static bool BossMode(WLIslandSettings cfg) { return cfg != null && cfg.enabled_ != 0 && cfg.expansionCurrency == 1; } /// 표 가격(열린 땅 수 기준). 증표 모드가 아니면 -1. /// #850 — 아직 안 연 땅부터 count 개를 여는 데 드는 증표 합(표 밖은 마지막 값). 증표 모드가 아니면 0. public static int CostForExpansions(WLIslandSettings cfg, int count) { if (!BossMode(cfg) || count <= 0) return 0; int sum = 0; for (int i = 0; i < count; i++) sum += Mathf.Max(0, TablePrice(cfg, cfg.expansionBaseUnlockedCount + i)); return sum; } public static int TablePrice(WLIslandSettings cfg, int unlocked) { if (!BossMode(cfg)) return -1; int idx = Mathf.Max(0, unlocked - cfg.expansionBaseUnlockedCount); if (cfg.expansionBossTokenPrices == null || cfg.expansionBossTokenPrices.Length == 0) return 1; return Mathf.Max(0, cfg.expansionBossTokenPrices[Mathf.Min(idx, cfg.expansionBossTokenPrices.Length - 1)]); } /// Purchaser.Init 훅 — 증표 모드면 표 가격으로 치환하고 그 판을 증표 판으로 기억한다. 던전 골드 발판은 코인 그대로. public static int PriceFor(FIPurchaser p, int fiPrice) { var cfg = WLIslandSettings.Instance; if (p == null || !BossMode(cfg)) return fiPrice; if (p.GetComponentInParent() != null) return fiPrice; // 던전 해금 발판(WLGoldPlatform) = 코인 var mgr = FIIslandManager.Instance; int unlocked = mgr != null && mgr.UnlockedIslands != null ? mgr.UnlockedIslands.Count : 0; int price = TablePrice(cfg, unlocked); s_bossPads.Add(p.GetInstanceID()); if (p.GetComponent() == null) p.gameObject.AddComponent(); PadsConverted++; LastLog = "확장 가격판 → " + WLBossToken.Name + " " + price + " (열린 땅 " + unlocked + " · 코인가 " + fiPrice + " 대체)"; WLIslandBridge.Log(cfg, LastLog); return price; } public static bool IsBossPad(FIPurchaser p) { return p != null && s_bossPads.Contains(p.GetInstanceID()); } /// Purchaser 구매 루프 훅 — 지불 가능한 잔액. public static int Balance(FIPurchaser p) { if (!IsBossPad(p)) { var mgr = FIIslandManager.Instance; return mgr != null ? mgr.Coin : 0; } int bal = WLBossToken.Get(); if (bal <= 0 && Time.unscaledTime - s_lastShortLogAt > 3f) { s_lastShortLogAt = Time.unscaledTime; WLIslandBridge.Log(WLIslandSettings.Instance, WLBossToken.Name + " 부족 — 던전 보스를 처치하면 얻는다"); } return bal; } /// Purchaser 구매 루프 훅 — 지불. public static void Spend(FIPurchaser p, int n) { if (n <= 0) return; if (!IsBossPad(p)) { var mgr = FIIslandManager.Instance; if (mgr != null) mgr.Coin -= n; return; } WLBossToken.TrySpend(n); } } /// /// 증표 가격판 표식 — 가격 글자를 증표 색으로 물들이고 「증표」 접두를 붙인다(FI 는 숫자만 쓴다). /// FI 가격판 폰트에는 한글이 없어 □□ 로 보였다(2026-10-09 캡처 실측) → 장비 UI 와 같은 한글 폰트로 바꾼다. 폰트를 못 찾으면 숫자만 둔다. /// public sealed class WLBossPricePad : MonoBehaviour { TMPro.TMP_Text _text; string _last; bool _fontTried; TMPro.TMP_FontAsset _kr; void LateUpdate() { if (_text == null) { _text = GetComponentInChildren(true); if (_text == null) return; } if (!_fontTried) { _fontTried = true; try { _kr = WL.Equip.WLEquipUi.ResolveKoreanFont(WL.Equip.WLEquipSettings.Instance); } catch (System.Exception) { _kr = null; } if (_kr != null) _text.font = _kr; } var cfg = WLIslandSettings.Instance; if (cfg != null) _text.color = cfg.bossTokenPriceColor; string cur = _text.text; if (cur == _last) return; if (_kr != null && !cur.StartsWith("증표")) _text.text = "증표 " + cur; _last = _text.text; } } /// 섬 화면 좌상단(FI 코인 HUD 아래) 「보스 증표 N」 라벨 — 섬에는 WL 재화 HUD 가 없어 따로 띄운다(2026-10-09 캡처 실측). public sealed class WLBossTokenHud : MonoBehaviour { public static WLBossTokenHud Instance { get; private set; } TMPro.TextMeshProUGUI _text; int _last = -1; float _t; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { if (Instance != null) return; var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.bossTokenPerBossClear <= 0) return; var go = new GameObject("WL_BossTokenHud", typeof(RectTransform)); go.layer = 5; DontDestroyOnLoad(go); Instance = go.AddComponent(); } void Start() { var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 50; var scaler = gameObject.AddComponent(); scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1080f, 1920f); scaler.matchWidthOrHeight = 0.5f; var box = new GameObject("Box", typeof(RectTransform), typeof(UnityEngine.UI.Image)); box.layer = 5; box.transform.SetParent(transform, false); var brt = (RectTransform)box.transform; brt.anchorMin = brt.anchorMax = new Vector2(0f, 1f); brt.pivot = new Vector2(0f, 1f); brt.anchoredPosition = new Vector2(470f, -40f); brt.sizeDelta = new Vector2(330f, 72f); // FI 「+1m coins」 오른쪽(좌상단 지도 버튼과 겹치지 않게 · 2차 캡처 실측) var img = box.GetComponent(); img.color = new Color(0f, 0f, 0f, 0.45f); img.raycastTarget = false; var t = new GameObject("Text", typeof(RectTransform)); t.layer = 5; t.transform.SetParent(box.transform, false); _text = t.AddComponent(); var trt = _text.rectTransform; trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = new Vector2(16f, 4f); trt.offsetMax = new Vector2(-12f, -4f); TryKoreanFont(); _text.fontSize = 32f; _text.alignment = TMPro.TextAlignmentOptions.MidlineLeft; _text.raycastTarget = false; _text.fontStyle = TMPro.FontStyles.Bold; var cfg = WLIslandSettings.Instance; _text.color = cfg != null ? cfg.bossTokenPriceColor : Color.white; Refresh(true); } void Update() { _t += Time.unscaledDeltaTime; if (_t < 0.5f) return; _t = 0f; Refresh(false); } bool _fontOk; /// 한글 폰트 — 씬 로드 직후엔 설정·폰트가 아직 없을 수 있어(2차 캡처 □□ 실측) 성공할 때까지 0.5초마다 다시 시도한다. void TryKoreanFont() { if (_fontOk || _text == null) return; // ResolveKoreanFont 는 한글 폰트를 못 찾으면 아무 폰트나 돌려준다(장비 UI 를 열기 전 섬에는 한글 라벨이 없다 · 3차 캡처 □□ 실측) → 한글 글자 보유를 직접 확인하고, 아니면 다음 틱에 다시 try { var kr = WL.Equip.WLEquipUi.ResolveKoreanFont(WL.Equip.WLEquipSettings.Instance); if (kr != null && kr.HasCharacter('증', true, true)) { _text.font = kr; _fontOk = true; } } catch (System.Exception) { } } void Refresh(bool force) { if (_text == null) return; TryKoreanFont(); int v = WLBossToken.Get(); bool island = CryingSnow.FarmingIsland.IslandManager.Instance != null; gameObject.SetActive(true); _text.transform.parent.gameObject.SetActive(island); if (!force && v == _last) return; _last = v; _text.text = WLBossToken.Name + " " + v + ""; } } }