VSClone/Assets/Scripts/View/GameApp.Popups.cs

264 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using UnityEngine.UI;
using VS.Sim;
using Vector2 = UnityEngine.Vector2;
// 팝업 — 원작 HousePopup · SkillPopup · ClosetPopup · RankUpPopup · ClaimDonatePopup · BuyNewHousePopup · SettingPopup 대응
namespace VS.View
{
public partial class GameApp
{
class Popup { public RectTransform root, panel, body; public Action refresh; public string id; }
Popup cur;
public bool PopupOpen => cur != null;
public string OpenPopupId => cur?.id;
Popup NewPopup(string id, string title, float height = 1500f, Color? head = null)
{
CloseAllPopups();
var p = new Popup { id = id };
var dim = UI.Btn(PopupLayer, "Dim", Art.Square, Pal.Dim, () => ClosePopup());
dim.GetComponent<PressScale>().enabled = false;
p.root = dim.GetComponent<RectTransform>().Fill();
var panel = UI.Img(p.root, id, Skin.PopupBg, Pal.Panel, true);
p.panel = panel.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -40), new Vector2(980, height));
var hd = UI.Img(p.panel, "Head", Skin.PopupTop ?? Skin.Round, head ?? Pal.Purple);
hd.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, 0), new Vector2(0, 130), new Vector2(0.5f, 1));
if (Skin.PopupTopLight != null) UI.Img(hd.transform, "Light", Skin.PopupTopLight, new Color(1f, 1f, 1f, 0.35f)).rectTransform.Fill(0, 0, 0, 0);
var tt = UI.Txt(hd.transform, "Title", title, 54, Color.white, TextAnchor.MiddleCenter, true); tt.rectTransform.Fill(110, 8, 110, 8);
var close = UI.Btn(hd.transform, "Close", Skin.On ? Skin.S("Button_Circle74_Bg") : Skin.Circle, Skin.On ? Color.white : Pal.Red, () => ClosePopup());
close.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-66, 0), new Vector2(88, 88));
if (Skin.On) UI.Icon(close.transform, "X", Skin.Icon("close"), Pal.Ink).rectTransform.Fill(24, 24, 24, 24);
else UI.Txt(close.transform, "X", "X", 50, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill();
p.body = UI.Rect("Body", p.panel).Fill(30, 30, 30, 150);
cur = p;
return p;
}
public void ClosePopup()
{
if (cur == null) return;
if (cur.id == "Closet") PreviewOutfit = -1;
Kill(cur.root.gameObject);
cur = null;
}
public void CloseAllPopups() { while (cur != null) ClosePopup(); }
void RefreshOpenPopup() { if (cur != null) cur.refresh?.Invoke(); }
void HookEvents()
{
G.TapEarned += amt =>
{
if (Headless) return;
FloatText(new Vector2(UnityEngine.Random.Range(-160f, 160f), 120f), "+" + Fmt.Num(amt), Pal.Yellow, 52, Skin.Icon("coin"));
};
G.HouseChanged += () => RebuildStage();
HookSystemEvents();
}
// ---------------- 공통 행(row) ----------------
class Row { public Image icon, bar; public Text name, level, info, price, timer; public Button buy, skip; public Image barFill; public GameObject lockObj; public Text lockText; }
Row MakeRow(RectTransform content, string name, float h = 250f)
{
var r = new Row();
var bg = UI.Img(content, name, Skin.Round, Pal.Row);
UI.SetLayoutHeight(bg, h);
r.icon = UI.Img(bg.transform, "Icon", null, Color.white); r.icon.preserveAspect = true;
r.icon.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(110, 0), new Vector2(180, 180));
r.name = UI.Txt(bg.transform, "Name", "", 40, Pal.Ink, TextAnchor.MiddleLeft);
r.name.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(220, -42), new Vector2(-520, 56), new Vector2(0, 0.5f));
r.level = UI.Txt(bg.transform, "Level", "", 32, Pal.Purple, TextAnchor.MiddleLeft);
r.level.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(220, -92), new Vector2(-520, 44), new Vector2(0, 0.5f));
r.info = UI.Txt(bg.transform, "Info", "", 28, new Color32(110, 100, 130, 255), TextAnchor.UpperLeft);
r.info.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(220, -150), new Vector2(-520, 80), new Vector2(0, 0.5f));
r.bar = UI.Bar(bg.transform, "Progress", Color.white, Pal.Pink, out r.barFill);
r.bar.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(220, 34), new Vector2(-520, 30), new Vector2(0, 0.5f));
r.buy = UI.TextBtn(bg.transform, "Buy", "", Pal.Green, 34, null, out r.price);
r.buy.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-150, 0), new Vector2(270, 150));
r.skip = UI.TextBtn(bg.transform, "Skip", "", Pal.Blue, 30, null, out r.timer);
r.skip.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-150, 0), new Vector2(270, 150));
r.lockObj = UI.Img(bg.transform, "Lock", Skin.RoundSoft, new Color(0.22f, 0.24f, 0.38f, 0.8f), true).gameObject;
((RectTransform)r.lockObj.transform).Fill();
UI.Icon(r.lockObj.transform, "LockIcon", Skin.Icon("lock"), Color.white).rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(110, 0), new Vector2(110, 110));
r.lockText = UI.Txt(r.lockObj.transform, "T", "", 36, Color.white, TextAnchor.MiddleLeft, true);
r.lockText.rectTransform.Fill(220, 10, 40, 10);
return r;
}
// ---------------- 방 (HousePopup · ItemHouse) ----------------
public void OpenHouse()
{
int h = G.S.house;
var p = NewPopup("House", L($"Content.Room.House{h + 1}.Name", $"방 {h + 1}") + $" ({h + 1}/{Tables.HouseCount})", 1560, Pal.Orange);
UI.Scroll(p.body, "List", out var content).GetComponent<RectTransform>().Fill(0, 0, 0, 0);
var rows = new List<(ItemRow it, Row r)>();
foreach (var it in G.Room.Items)
{
var r = MakeRow(content, "Item" + it.Index);
var item = it;
r.buy.onClick.AddListener(() => { var res = G.BuyRoomItem(item.Index); if (res != BuyResult.Ok) Toast(BuyMsg(res)); RefreshAll(); });
r.skip.onClick.AddListener(() => { G.SkipRoomUpgrade(item.Index); Toast(L("UI.Common.TestGrant", "시험 지급: 대기 시간 건너뜀")); RefreshAll(); });
rows.Add((it, r));
}
p.refresh = () =>
{
foreach (var (it, r) in rows)
{
int lv = G.RoomLevel(it.Index), max = it.Levels.Count - 1;
int st = Math.Max(0, VisualIndex(G.EffRoomLevel(it.Index), max));
r.icon.sprite = it.Index == 10 ? Skin.Icon("house") : Art.Prop(h, it.Index, it.Name, st);
r.name.text = ItemName(h, it);
r.level.text = lv < 0 ? L("UI.Common.NotOwned", "미보유") : $"Lv.{lv + 1} / {max + 1}";
var nx = lv + 1 <= max ? it.Levels[lv + 1] : null;
var curL = G.RoomLevelData(it.Index, lv);
r.info.text = nx != null
? $"탭 +{(nx.TapRate - Math.Max(0, curL.TapRate)) / 100f:0.#}% 패시브 +{(nx.PassRate - Math.Max(0, curL.PassRate)) / 100f:0.#}% 팔로워 +{Fmt.Num(Big.P(nx.Follower))}"
: L("UI.Common.Max", "최대 레벨");
int nextKey = lv < 24 ? 24 : (lv < 49 ? 49 : max);
int prevKey = lv < 24 ? -1 : (lv < 49 ? 24 : 49);
r.barFill.fillAmount = lv >= max ? 1f : Mathf.Clamp01((lv - prevKey) / (float)Math.Max(1, nextKey - prevKey));
bool upg = G.RoomUpgrading(it.Index);
bool unlocked = G.RoomUnlocked(it.Index);
r.lockObj.SetActive(!unlocked);
if (!unlocked) { var dep = G.RoomItem(it.IndexDependency); r.lockText.text = (dep != null ? ItemName(h, dep) : "?") + " " + L("UI.Common.RequireBuy", "구매 필요"); }
r.skip.gameObject.SetActive(upg);
r.buy.gameObject.SetActive(!upg);
if (upg) r.timer.text = Fmt.Time(G.RoomUpgradeRemain(it.Index)) + "\n건너뛰기";
if (lv >= max) { r.price.text = "MAX"; r.buy.interactable = false; }
else { var price = G.RoomNextPrice(it.Index); r.price.text = (lv < 0 ? "구매\n" : "업그레이드\n") + Fmt.Num(price); r.buy.interactable = price <= G.Coin; }
}
};
p.refresh();
}
string BuyMsg(BuyResult r) => r switch
{
BuyResult.NoMoney => L("UI.Common.NotEnoughCoin", "코인이 부족합니다"),
BuyResult.Locked => L("UI.Common.Locked", "잠겨 있습니다"),
BuyResult.Max => L("UI.Common.Max", "최대 레벨"),
BuyResult.Busy => "업그레이드 진행 중",
_ => ""
};
// ---------------- 스킬 (SkillPopup · ItemSkill) ----------------
public void OpenSkill()
{
var p = NewPopup("Skill", L("UI.Skill.Title", "스킬"), 1500, Pal.Purple);
UI.Scroll(p.body, "List", out var content).GetComponent<RectTransform>().Fill();
var rows = new List<(ItemRow it, Row r)>();
foreach (var it in T.Skills.Items)
{
var r = MakeRow(content, "Skill" + it.Index);
var item = it;
r.buy.onClick.AddListener(() => { var res = G.BuySkill(item.Index); if (res != BuyResult.Ok) Toast(BuyMsg(res)); RefreshAll(); });
r.skip.onClick.AddListener(() => { G.SkipSkillUpgrade(item.Index); Toast("시험 지급: 대기 시간 건너뜀"); RefreshAll(); });
rows.Add((it, r));
}
p.refresh = () =>
{
foreach (var (it, r) in rows)
{
int lv = G.SkillLevel(it.Index), max = it.Levels.Count;
r.icon.sprite = Skin.Icon(new[] { "chat", "star", "fire", "skill", "heart" }[it.Index % 5]);
r.name.text = SkillName(it);
r.level.text = $"Lv.{lv} / {max}";
var nx = lv < max ? it.Levels[lv] : null;
r.info.text = nx != null ? $"탭 {nx.TapRate / 100f:0.#}% 패시브 {nx.PassRate / 100f:0.#}% 팔로워 +{Fmt.Num(Big.P(nx.Follower))}" : "최대 레벨";
r.barFill.fillAmount = max > 0 ? lv / (float)max : 1f;
bool cond = G.CanBuySkill(it.Index) || G.SkillMaxed(it.Index);
r.lockObj.SetActive(!cond);
if (!cond && nx != null) r.lockText.text = L($"Content.Room.House{nx.Condition + 1}.Name", $"방 {nx.Condition + 1}") + " 필요";
bool upg = G.SkillUpgrading(it.Index);
r.skip.gameObject.SetActive(upg); r.buy.gameObject.SetActive(!upg);
if (upg) r.timer.text = Fmt.Time(G.SkillUpgradeRemain(it.Index)) + "\n건너뛰기";
if (nx == null) { r.price.text = "MAX"; r.buy.interactable = false; }
else { var price = G.SkillNextPrice(it.Index); r.price.text = "배우기\n" + Fmt.Num(price); r.buy.interactable = price <= G.Coin; }
}
};
p.refresh();
}
// ---------------- 랭크 업 (RankUpPopup) ----------------
public void OpenRankUp()
{
int before = G.S.rankIndex;
var reward = G.ApplyRankUp();
var rk = G.CurrentRank();
var p = NewPopup("RankUp", L("UI.RankUp.Title", "랭크 업!"), 900, Pal.Yellow);
var star = UI.Icon(p.body, "Star", Skin.Icon("star"), Color.white);
star.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -140), new Vector2(240, 240));
var t = UI.Txt(p.body, "Rank", T.Loc.Get($"Content.Rank.{rk.Index}.Name", rk.Rank), 64, Pal.Purple);
t.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -320), new Vector2(0, 90));
var bonus = UI.Txt(p.body, "Boost", $"수익 배율 x{rk.RankBoostRate} · 보상 {Fmt.Num(reward)} 코인", 38, Pal.Ink);
bonus.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -410), new Vector2(0, 70));
var ok = UI.TextBtn(p.body, "Claim", "받기", Pal.Green, 44, () => ClosePopup(), out _);
ok.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(-200, 90), new Vector2(340, 140));
var x2 = UI.TextBtn(p.body, "ClaimX2", "x2 받기 (시험)", Pal.Pink, 40, () => { G.AddCoin(reward); ClosePopup(); }, out _);
x2.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(200, 90), new Vector2(340, 140));
G.OnRankUp(before, G.S.rankIndex);
}
// ---------------- 후원 받기 (ClaimDonatePopup) ----------------
public void OpenClaimDonate()
{
var amount = G.DonateClaimable;
if (amount <= 0) return;
var p = NewPopup("ClaimDonate", L("UI.Gameplay.ClaimDonate", "후원 받기"), 820, Pal.Green);
UI.Icon(p.body, "Coin", Skin.Icon("coin"), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -130), new Vector2(200, 200));
var amt = UI.Txt(p.body, "Amount", Fmt.Num(amount), 72, Pal.Ink);
amt.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -300), new Vector2(0, 100));
var ok = UI.TextBtn(p.body, "Claim", "받기", Pal.Green, 44, () => { var got = G.ClaimDonate(1); FloatText(new Vector2(0, 200), "+" + Fmt.Num(got), Pal.Yellow, 64, Skin.Icon("coin")); ClosePopup(); }, out _);
ok.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(-200, 80), new Vector2(340, 140));
var x2 = UI.TextBtn(p.body, "ClaimX2", "x2 받기 (시험)", Pal.Pink, 40, () => { var got = G.ClaimDonate(2); FloatText(new Vector2(0, 200), "+" + Fmt.Num(got), Pal.Yellow, 64, Skin.Icon("coin")); ClosePopup(); }, out _);
x2.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(200, 80), new Vector2(340, 140));
p.refresh = () => amt.text = Fmt.Num(G.DonateClaimable);
}
// ---------------- 새 집 (BuyNewHousePopup) ----------------
public void OpenBuyNewHouse()
{
if (!G.HasNextHouse) { Toast("마지막 방입니다"); return; }
int nh = G.S.house + 1;
var p = NewPopup("BuyNewHouse", L("UI.NewHouse.Title", "새 방"), 900, Pal.Orange);
UI.Img(p.body, "House", Art.RoomBg(nh, 0), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -230), new Vector2(330, 440));
var nm = UI.Txt(p.body, "Name", L($"Content.Room.House{nh + 1}.Name", $"방 {nh + 1}"), 54, Pal.Ink);
nm.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 260), new Vector2(0, 70));
var price = G.NextHousePrice();
var b = UI.TextBtn(p.body, "Buy", "이사하기\n" + Fmt.Num(price), Pal.Green, 40, () =>
{
if (G.BuyNextHouse()) { ClosePopup(); Toast("새 방으로 이사했습니다"); RebuildStage(); RefreshAll(); }
else Toast("모든 아이템을 최대로 올리고 코인이 있어야 합니다");
}, out _);
b.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 90), new Vector2(420, 150));
p.refresh = () => b.interactable = G.CanBuyNextHouse();
p.refresh();
}
// ---------------- 설정 (SettingPopup + 시험 도구) ----------------
public void OpenSettings()
{
var p = NewPopup("Setting", L("UI.Setting.Title", "설정"), 1200, Pal.Purple);
var info = UI.Txt(p.body, "Info", "Viral Streamer 클론 (로컬 R&D)\n원작 1.1.52 표·공식 재현 · 그림 = 코드 생성", 34, Pal.Ink);
info.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -80), new Vector2(0, 140));
float y = -260;
void Act(string label, Color c, Action a) { var b = UI.TextBtn(p.body, label, label, c, 40, () => { a(); RefreshAll(); }, out _); b.GetComponent<RectTransform>().Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, y), new Vector2(760, 130)); y -= 160; }
Act("시험: 코인 +(방치 수입 1시간)", Pal.Blue, () => G.Coin += new BigInteger(decimal.Truncate(G.PassGps() * 3600m)) + 100);
Act("시험: 티켓 +10", Pal.Pink, () => G.S.ticket += 10);
Act("시험: 시간 +10분 (타이머)", Pal.Orange, () => G.DebugAdvance(600));
Act("데이터 초기화", Pal.Red, () => ResetSave());
}
// ---------------- 알림 ----------------
public void Toast(string s)
{
if (string.IsNullOrEmpty(s) || Headless) return;
FloatText(new Vector2(0, 300), s, Color.white, 44, null, true);
}
}
}