// ─────────────────────────────────────────────────────────────────────────────
// 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