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().enabled = false; p.root = dim.GetComponent().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().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().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().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().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().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().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().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().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().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().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().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); } } }