Project_WL/Assets/WL/Island/WLBossToken.cs

225 lines
12 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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 (세이브 초기화)"; }
/// <summary>던전 스테이지 클리어 훅(WLIslandBridge.OnStageCleared) — 보스 스테이지 승리만 센다.</summary>
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);
}
}
/// <summary>FI Purchaser 훅 — 가격판이 쓰는 재화(코인 / 보스 증표)와 가격.</summary>
public static class WLLandCurrency
{
static readonly HashSet<int> s_bossPads = new HashSet<int>(); // 증표 모드로 바뀐 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; }
/// <summary>표 가격(열린 땅 수 기준). 증표 모드가 아니면 -1.</summary>
/// <summary>#850 — 아직 안 연 땅부터 count 개를 여는 데 드는 증표 합(표 밖은 마지막 값). 증표 모드가 아니면 0.</summary>
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)]);
}
/// <summary>Purchaser.Init 훅 — 증표 모드면 표 가격으로 치환하고 그 판을 증표 판으로 기억한다. 던전 골드 발판은 코인 그대로.</summary>
public static int PriceFor(FIPurchaser p, int fiPrice)
{
var cfg = WLIslandSettings.Instance;
if (p == null || !BossMode(cfg)) return fiPrice;
if (p.GetComponentInParent<WLGoldPlatform>() != 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<WLBossPricePad>() == null) p.gameObject.AddComponent<WLBossPricePad>();
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()); }
/// <summary>Purchaser 구매 루프 훅 — 지불 가능한 잔액.</summary>
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;
}
/// <summary>Purchaser 구매 루프 훅 — 지불.</summary>
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);
}
}
/// <summary>
/// 증표 가격판 표식 — 가격 글자를 증표 색으로 물들이고 「증표」 접두를 붙인다(FI 는 숫자만 쓴다).
/// FI 가격판 폰트에는 한글이 없어 □□ 로 보였다(2026-10-09 캡처 실측) → 장비 UI 와 같은 한글 폰트로 바꾼다. 폰트를 못 찾으면 숫자만 둔다.
/// </summary>
public sealed class WLBossPricePad : MonoBehaviour
{
TMPro.TMP_Text _text; string _last; bool _fontTried; TMPro.TMP_FontAsset _kr;
void LateUpdate()
{
if (_text == null) { _text = GetComponentInChildren<TMPro.TMP_Text>(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;
}
}
/// <summary>섬 화면 좌상단(FI 코인 HUD 아래) 「보스 증표 N」 라벨 — 섬에는 WL 재화 HUD 가 없어 따로 띄운다(2026-10-09 캡처 실측).</summary>
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<WLBossTokenHud>();
}
void Start()
{
var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 50;
var scaler = gameObject.AddComponent<UnityEngine.UI.CanvasScaler>();
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<UnityEngine.UI.Image>(); 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<TMPro.TextMeshProUGUI>();
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;
/// <summary>한글 폰트 — 씬 로드 직후엔 설정·폰트가 아직 없을 수 있어(2차 캡처 □□ 실측) 성공할 때까지 0.5초마다 다시 시도한다.</summary>
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 + " <b>" + v + "</b>";
}
}
}