264 lines
17 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|