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

401 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using UnityEngine.UI;
using VS.Sim;
using Random = UnityEngine.Random;
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;
// 부가 시스템 화면 — 선물 슬롯·팝업 · 부스터 · 방치 보상 · 퀘스트 · 일일 보상 · 티켓 상점 · 채팅 · 시청자 수 (systems.md)
namespace VS.View
{
public partial class GameApp
{
static readonly string[] GiftEnum = { "NoAds", "SmallMoneyPack", "BigMoneyPack", "ReducePrice", "X4TapIncome", "X4PassiveIncome", "X10Income" };
static readonly string[] GiftBadge = { "AD", "$", "$$", "%", "x4", "x4", "x10" };
readonly List<(Button b, Image icon, Text badge, Text time)> giftViews = new List<(Button, Image, Text, Text)>();
string GiftTitle(int t) => T.Loc.Get($"Content.Gift.{GiftEnum[t]}.Title", T.Gift(t)?.Title ?? GiftEnum[t]);
string GiftDesc(int t)
{
string d = T.Loc.Get($"Content.Gift.{GiftEnum[t]}.Description", T.Gift(t)?.PopupDescription ?? "");
return d.Replace("{0}", Fmt.Time(G.GiftEffectDuration(t)));
}
// ---------------- 선물 슬롯 (GiftSlotView) ----------------
void BuildGiftSlots(RectTransform parent)
{
int n = Mathf.Max(1, T.Cfg.gift.maxVisibleSlots);
for (int i = 0; i < n; i++)
{
int slot = i;
var b = UI.Btn(parent, "GiftSlot" + i, Skin.Dark, Color.white, () => OnGiftSlot(slot));
b.GetComponent<RectTransform>().sizeDelta = new Vector2(140, 140);
var ic = UI.Icon(b.transform, "Icon", Skin.Icon("gift"), Color.white); ic.rectTransform.Fill(18, 40, 18, 12);
var badge = UI.Txt(b.transform, "Badge", "", 34, Color.white, TextAnchor.MiddleCenter, true);
badge.rectTransform.Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 24), new Vector2(140, 50));
var tm = UI.Txt(b.transform, "Time", "", 26, Color.white, TextAnchor.MiddleCenter, true);
tm.rectTransform.Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, -16), new Vector2(170, 40));
giftViews.Add((b, ic, badge, tm));
}
}
void OnGiftSlot(int slot)
{
if (G.GiftSlot == null || slot >= G.GiftSlot.Length) return;
int t = G.GiftSlot[slot];
if (t < 0 || G.IsGiftEffectActive(t)) return;
OpenGiftPopup(t);
}
void RefreshGiftSlots()
{
for (int i = 0; i < giftViews.Count; i++)
{
var (b, icon, badge, time) = giftViews[i];
int t = G.GiftSlot != null && i < G.GiftSlot.Length ? G.GiftSlot[i] : -1;
b.gameObject.SetActive(t >= 0);
if (t < 0) continue;
badge.text = GiftBadge[t];
float rem = G.GiftEffectRemain(t);
time.text = rem > 0 ? Fmt.Time(rem) : (t == GiftTypes.NoAds && G.Gifts.TryGetValue(t, out var gs) && gs.Rv > 0 ? $"{gs.Rv}/{T.X.giftNoAdsVideos}" : "");
b.GetComponent<Image>().color = rem > 0 ? new Color32(255, 225, 150, 255) : (Color32)Color.white;
}
}
public void OpenGiftPopup(int t)
{
var p = NewPopup("Gift", GiftTitle(t), 980, Pal.Purple);
var ic = UI.Icon(p.body, "Icon", Skin.Icon("gift"), Color.white);
ic.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -120), new Vector2(200, 200));
UI.Txt(ic.transform, "Badge", GiftBadge[t], 64, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill();
var desc = UI.Txt(p.body, "Desc", GiftDesc(t), 38, Pal.Ink);
desc.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -300), new Vector2(-40, 130));
string amount = GiftTypes.IsInstant(t) ? Fmt.Num(G.MoneyPackReward(t)) + " 코인" : "";
var amt = UI.Txt(p.body, "Amount", amount, 54, Pal.PinkDark);
amt.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -410), new Vector2(0, 80));
float x = t == GiftTypes.SmallMoneyPack ? -300 : -170;
void Btn(string label, Color c, Action a) { var b = UI.TextBtn(p.body, label, label, c, 36, () => { a(); RefreshAll(); }, out _); b.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(x, 80), new Vector2(280, 140)); x += 300; }
if (t == GiftTypes.SmallMoneyPack) Btn("받기", Pal.Green, () => { ShowGot(G.ClaimGift(t, false)); ClosePopup(); });
Btn("광고 보기\n(시험)", Pal.Pink, () => { ShowGot(G.ClaimGift(t, true)); ClosePopup(); });
Btn("티켓 1장", Pal.Blue, () => { if (!G.TrySpendTicket()) { Toast("티켓이 부족합니다"); return; } ShowGot(G.ClaimGift(t, true)); ClosePopup(); });
var close = p.panel.Find("Head/Close")?.GetComponent<Button>();
if (close != null) { close.onClick.RemoveAllListeners(); close.onClick.AddListener(() => { G.DismissGift(t); ClosePopup(); }); }
}
void ShowGot(BigInteger v) { if (v > 0) FloatText(new Vector2(0, 260), "+" + Fmt.Num(v), Pal.Yellow, 64, Skin.Icon("coin")); }
void OpenFreeGiftPopup()
{
var p = NewPopup("FreeGift", L("UI.FreeGift.Title", "무료 선물"), 800, Pal.Pink);
UI.Icon(p.body, "Ticket", Skin.Icon("ticket"), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -130), new Vector2(200, 200));
UI.Txt(p.body, "T", $"티켓 {T.Cfg.remote.FreeGiftTicketAmount}장을 드립니다!\n티켓으로 광고 없이 선물을 받을 수 있어요", 36, Pal.Ink).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -320), new Vector2(-40, 140));
var b = UI.TextBtn(p.body, "Claim", "받기", Pal.Green, 44, () => { G.ClaimFreeGiftPopup(); ClosePopup(); RefreshAll(); }, out _);
b.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 80), new Vector2(360, 140));
}
// ---------------- 부스터 (Boosterx2HeartPopup · BoosterAutoClickPopup) ----------------
public void OpenBoosterX2() => OpenBooster(true);
public void OpenBoosterAuto() => OpenBooster(false);
void OpenBooster(bool x2)
{
var r = T.Cfg.remote;
string title = x2 ? L("Gameplay.Booster.DoubleCoin.Title", "코인 2배") : L("Gameplay.Booster.AutoClick.Title", "자동 클릭");
string desc = x2 ? L("Gameplay.Booster.DoubleCoin.Description", "{0} 동안 코인 2배") : L("Gameplay.Booster.AutoClick.Description", "{0} 동안 자동 클릭");
var p = NewPopup(x2 ? "BoosterX2" : "BoosterAuto", title, 860, x2 ? Pal.Red : Pal.Blue);
UI.Icon(p.body, "Icon", Skin.Icon(x2 ? "x2" : "auto"), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -120), new Vector2(200, 200));
var d = UI.Txt(p.body, "Desc", desc.Replace("{0}", Fmt.Time(x2 ? r.X2CoinRewardTime : r.AutoTapRewardTime)), 40, Pal.Ink);
d.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -300), new Vector2(-40, 110));
var remain = UI.Txt(p.body, "Remain", "", 34, Pal.PinkDark);
remain.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -390), new Vector2(0, 60));
Action act = () => { if (x2) G.ActivateX2Boost(); else G.ActivateAutoClick(); ClosePopup(); RefreshAll(); };
var ad = UI.TextBtn(p.body, "Ad", "광고 보기\n(시험)", Pal.Pink, 36, () => act(), out _);
ad.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(-170, 80), new Vector2(300, 140));
var tk = UI.TextBtn(p.body, "Ticket", "티켓 1장", Pal.Blue, 36, () => { if (G.TrySpendTicket()) act(); else Toast("티켓이 부족합니다"); }, out _);
tk.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(170, 80), new Vector2(300, 140));
p.refresh = () => { float rm = x2 ? G.AdsBoostRemain : G.AutoClickRemain; remain.text = rm > 0 ? "남은 시간 " + Fmt.Time(rm) + " (누적됨)" : ""; };
p.refresh();
}
// ---------------- 방치 보상 (ClaimAfkPopup · 룰렛 ×2 ×3 ×5 ×3 ×2) ----------------
float afkArrow; bool afkLocked;
public void OpenAfkPopup(bool auto)
{
var pending = G.AfkPending;
if (pending <= 0) { if (!auto) Toast("받을 방치 보상이 없습니다"); return; }
var p = NewPopup("ClaimAfk", L("UI.AfkGift.Title", "방치 보상"), 1100, Pal.Blue);
var msg = UI.Txt(p.body, "Msg", $"자리를 비운 {Fmt.Time(G.AfkSeconds)} 동안 번 코인", 36, Pal.Ink);
msg.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -60), new Vector2(-40, 70));
var amt = UI.Txt(p.body, "Amount", Fmt.Num(pending), 80, Pal.PinkDark);
amt.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -170), new Vector2(0, 110));
var bar = UI.Rect("Roulette", p.body).Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -330), new Vector2(820, 110));
var mul = T.X.afkMultipliers;
float w = 820f / mul.Length;
var segs = new List<Image>();
for (int i = 0; i < mul.Length; i++)
{
var s = UI.Img(bar, "Seg" + i, Skin.Round, mul[i] >= 5 ? Pal.Orange : (mul[i] >= 3 ? Pal.Purple : Pal.Pink));
s.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(w * i + w / 2, 0), new Vector2(w - 8, 100));
UI.Txt(s.transform, "T", "x" + mul[i], 44, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Fill();
segs.Add(s);
}
var arrow = UI.Icon(bar, "Arrow", Skin.Icon("up"), Color.white);
arrow.rectTransform.Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(0, -50), new Vector2(70, 70));
afkArrow = 0; afkLocked = false;
int SegAt() => Mathf.Clamp(Mathf.FloorToInt(Mathf.PingPong(afkArrow / Mathf.Max(0.01f, T.X.afkArrowMoveDuration), 1f) * mul.Length), 0, mul.Length - 1);
var claim = UI.TextBtn(p.body, "Claim", "받기", Pal.Green, 40, () => { ShowGot(G.ClaimAfk(1)); ClosePopup(); RefreshAll(); }, out _);
claim.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(-300, 80), new Vector2(280, 140));
var ad = UI.TextBtn(p.body, "Ad", "광고 보기\n(시험)", Pal.Pink, 34, () => { afkLocked = true; ShowGot(G.ClaimAfk(mul[SegAt()])); ClosePopup(); RefreshAll(); }, out _);
ad.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 80), new Vector2(280, 140));
var tk = UI.TextBtn(p.body, "Ticket", "티켓 1장", Pal.Blue, 34, () => { if (!G.TrySpendTicket()) { Toast("티켓이 부족합니다"); return; } afkLocked = true; ShowGot(G.ClaimAfk(mul[SegAt()])); ClosePopup(); RefreshAll(); }, out _);
tk.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(300, 80), new Vector2(280, 140));
p.refresh = () =>
{
if (!afkLocked) afkArrow += 0.1f;
float f = Mathf.PingPong(afkArrow / Mathf.Max(0.01f, T.X.afkArrowMoveDuration), 1f);
arrow.rectTransform.anchoredPosition = new Vector2(f * 820f, -50);
int k = SegAt();
for (int i = 0; i < segs.Count; i++) segs[i].transform.localScale = Vector3.one * (i == k ? 1.08f : 1f);
};
}
// ---------------- 퀘스트 (TaskPopup) ----------------
string QuestText(SaveQuest q)
{
var r = G.QuestRowOf(q.id);
if (r == null) return "";
int idx = r.conditionType < T.X.questTemplateIndex.Length ? T.X.questTemplateIndex[r.conditionType] : 0;
string tpl = T.Loc.Get("Task.Index" + idx, "{0}");
string a0 = r.param1, a1 = r.param2;
switch (r.conditionType)
{
case QuestCond.ConsumeCoin: case QuestCond.IncreaseCoin: case QuestCond.ConsumeCoinDynamic: case QuestCond.IncreaseCoinDynamic:
a0 = Fmt.Num(Big.D(q.target)); break;
case QuestCond.UnlockRoomItem: case QuestCond.UpgradeRoomItem: a0 = TargetName(r.param1); break;
case QuestCond.UnlockHouse: a0 = ((int)Big.D(r.param1) + 1).ToString(); break;
case QuestCond.ClaimGiftMoney: case QuestCond.ClaimGiftBuff:
{ int gt = (int)Big.D(r.param1); a0 = gt > 0 && gt < GiftEnum.Length ? GiftTitle(gt) : "선물"; break; }
}
return tpl.Replace("{0}", a0).Replace("{1}", a1);
}
string TargetName(string target)
{
var p = (target ?? "").Split('_');
if (p[0] == "0") return p.Length >= 2 ? "의상" : L("UI.Closet.Title", "의상");
if (p[0] == "1" && p.Length >= 3 && int.TryParse(p[1], out var h) && int.TryParse(p[2], out var i) && h >= 0 && h < Tables.HouseCount)
{
var it = T.Rooms[h].Items.Find(x => x.Index == i);
return it != null ? ItemName(h, it) : target;
}
if (p[0] == "2" && p.Length >= 2 && int.TryParse(p[1], out var s)) { var it = T.Skills.Items.Find(x => x.Index == s); return it != null ? SkillName(it) : target; }
return target;
}
public void OpenTasks()
{
var p = NewPopup("Task", L("UI.Task.Title", "퀘스트"), 1100, Pal.Green);
var list = UI.Rect("List", p.body).Fill();
var vl = list.gameObject.AddComponent<VerticalLayoutGroup>();
vl.spacing = 24; vl.childControlHeight = true; vl.childControlWidth = true; vl.childForceExpandHeight = false; vl.padding = new RectOffset(0, 0, 10, 10);
var rows = new List<(Text t, Image fill, Text prog, Button b, Text bl)>();
for (int i = 0; i < Mathf.Max(1, T.X.questSlots); i++)
{
var bg = UI.Img(list, "Quest" + i, Skin.Round, Pal.Row);
UI.SetLayoutHeight(bg, 250);
var tx = UI.Txt(bg.transform, "Text", "", 38, Pal.Ink, TextAnchor.MiddleLeft);
tx.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(30, -60), new Vector2(-380, 80), new Vector2(0, 0.5f));
var bar = UI.Bar(bg.transform, "Bar", Color.white, Pal.Green, out var fill);
bar.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(30, 70), new Vector2(-380, 40), new Vector2(0, 0.5f));
var pr = UI.Txt(bar.transform, "P", "", 26, Pal.Ink); pr.rectTransform.Fill();
int slot = i;
var b = UI.TextBtn(bg.transform, "Claim", "", Pal.Green, 32, () =>
{
var act = G.ActiveQuests();
if (slot >= act.Count) return;
var got = G.ClaimQuest(act[slot].id);
if (got != null) Toast("보상: " + got);
RefreshAll();
}, out var bl);
b.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-170, 0), new Vector2(300, 150));
rows.Add((tx, fill, pr, b, bl));
}
p.refresh = () =>
{
var act = G.ActiveQuests();
for (int i = 0; i < rows.Count; i++)
{
var (t, fill, prog, b, bl) = rows[i];
bool has = i < act.Count;
b.transform.parent.gameObject.SetActive(has);
if (!has) continue;
var q = act[i]; var r = G.QuestRowOf(q.id);
t.text = QuestText(q);
decimal pv = Big.D(q.progress), tg = Math.Max(1m, Big.D(q.target));
fill.fillAmount = Mathf.Clamp01((float)(pv / tg));
prog.text = $"{Fmt.Num(Math.Min(pv, tg))} / {Fmt.Num(tg)}";
var rw = G.QuestRewardPreview(q);
bl.text = (q.state == 2 ? "받기\n" : "보상\n") + (r != null && r.conditionType == QuestCond.UnlockHouse ? $"티켓 {rw}" : Fmt.Num(rw));
b.interactable = q.state == 2;
}
};
p.refresh();
}
// ---------------- 일일 보상 (DailyRewardPopup) ----------------
public void OpenDaily()
{
var p = NewPopup("DailyReward", L("UI.DailyReward.Title", "일일 보상"), 1200, Pal.Orange);
var grid = UI.Rect("Grid", p.body).Fill(10, 220, 10, 20);
var gl = grid.gameObject.AddComponent<GridLayoutGroup>();
gl.cellSize = new Vector2(210, 260); gl.spacing = new Vector2(16, 16); gl.constraint = GridLayoutGroup.Constraint.FixedColumnCount; gl.constraintCount = 4; gl.childAlignment = TextAnchor.UpperCenter;
var cells = new List<Image>();
for (int i = 0; i < T.X.dailyRewards.Count; i++)
{
var d = T.X.dailyRewards[i];
var c = UI.Img(grid, "Day" + (i + 1), Skin.Round, Color.white);
UI.Txt(c.transform, "Day", $"{i + 1}일차", 30, Pal.Ink).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -30), new Vector2(0, 44));
var ic = UI.Img(c.transform, "Icon", d.kind == 0 ? Skin.Icon("ticket") : Art.Idol(d.amount, 0), Color.white); ic.preserveAspect = true;
ic.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -6), new Vector2(130, 130));
UI.Txt(c.transform, "Amt", d.kind == 0 ? "x" + d.amount : "의상", 30, Pal.PinkDark).rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 28), new Vector2(0, 44));
cells.Add(c);
}
var b = UI.TextBtn(p.body, "Claim", "받기", Pal.Green, 44, () => { var d = G.ClaimDaily(); if (d != null) Toast(d.kind == 0 ? $"티켓 {d.amount}장" : "새 의상!"); RefreshAll(); }, out var bl);
b.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 90), new Vector2(400, 140));
p.refresh = () =>
{
for (int i = 0; i < cells.Count; i++)
cells[i].color = i < G.S.dailyClaimedDays ? new Color32(210, 240, 214, 255) : (i == G.S.dailyClaimedDays && G.DailyClaimable ? new Color32(255, 236, 170, 255) : Color.white);
b.interactable = G.DailyClaimable;
bl.text = G.DailyClaimable ? "받기" : (G.S.dailyClaimedDays >= cells.Count ? "모두 받음" : "내일 다시");
};
p.refresh();
}
// ---------------- 티켓 상점 (TicketShopPopup) ----------------
public void OpenTicketShop()
{
var p = NewPopup("TicketShop", L("UI.Shop.TicketTitle", "티켓 상점"), 1500, Pal.Pink);
var online = UI.Img(p.body, "Online", Skin.Round, new Color32(255, 240, 248, 255));
online.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, 0), new Vector2(0, 260), new Vector2(0.5f, 1));
var ot = UI.Txt(online.transform, "T", "", 36, Pal.Ink, TextAnchor.MiddleLeft); ot.rectTransform.Fill(30, 20, 360, 20);
var ob = UI.TextBtn(online.transform, "Claim", "받기", Pal.Green, 36, () => { if (G.ClaimOnline()) Toast("티켓 +1"); RefreshAll(); }, out _);
ob.GetComponent<RectTransform>().Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-170, 0), new Vector2(300, 140));
var daily = UI.TextBtn(p.body, "Daily", "일일 보상", Pal.Orange, 38, () => OpenDaily(), out _);
daily.GetComponent<RectTransform>().Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -330), new Vector2(500, 120));
var grid = UI.Rect("Packs", p.body).Fill(20, 20, 20, 430);
var gl = grid.gameObject.AddComponent<GridLayoutGroup>();
gl.cellSize = new Vector2(280, 300); gl.spacing = new Vector2(20, 20); gl.constraint = GridLayoutGroup.Constraint.FixedColumnCount; gl.constraintCount = 3; gl.childAlignment = TextAnchor.UpperCenter;
foreach (var n in T.X.ticketPacks)
{
int amount = n;
var b = UI.Btn(grid, "Pack" + n, Skin.Round, Color.white, () => { G.S.ticket += amount; Toast($"시험 지급: 티켓 {amount}장"); RefreshAll(); });
UI.Icon(b.transform, "I", Skin.Icon("ticket"), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -100), new Vector2(150, 150));
UI.Txt(b.transform, "N", "x" + n, 44, Pal.PinkDark).rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 90), new Vector2(0, 60));
UI.Txt(b.transform, "P", "시험 지급", 26, Pal.Gray).rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 36), new Vector2(0, 40));
}
p.refresh = () =>
{
ob.interactable = G.OnlineClaimable;
ot.text = G.S.onlineClaimed >= 3 ? "오늘 온라인 티켓을 모두 받았습니다"
: G.OnlineClaimable ? "온라인 보상: 티켓 1장 받을 수 있음" : $"온라인 보상 {G.S.onlineClaimed}/3 · 다음까지 {Fmt.Time(G.OnlineNextAt - G.S.onlineSeconds)}";
};
p.refresh();
}
// ---------------- HUD 갱신 ----------------
void RefreshSystemsHud()
{
var sb = new System.Text.StringBuilder();
if (G.AdsBoostRemain > 0) sb.Append($"♥ x2 {Fmt.Time(G.AdsBoostRemain)}\n");
if (G.AutoClickRemain > 0) sb.Append($"⚡ 자동 {Fmt.Time(G.AutoClickRemain)}\n");
foreach (var t in new[] { GiftTypes.X10Income, GiftTypes.X4TapIncome, GiftTypes.X4PassiveIncome, GiftTypes.ReducePrice })
if (G.IsGiftEffectActive(t)) sb.Append($"{GiftTitle(t)} {Fmt.Time(G.GiftEffectRemain(t))}\n");
boostText.text = sb.ToString();
afkBtn.gameObject.SetActive(G.AfkPending > 0);
taskDot.gameObject.SetActive(G.AnyQuestClaimable());
RefreshGiftSlots();
if (!PopupOpen && !Headless)
{
if (G.PendingFreeGiftPopup && !G.S.freeGiftPopupClaimed) OpenFreeGiftPopup();
else if (G.AfkPending > 0 && !afkShownThisSession) { afkShownThisSession = true; OpenAfkPopup(true); }
else if (G.DailyClaimable && !dailyShownThisSession && G.S.playSeconds > 5f) { dailyShownThisSession = true; OpenDaily(); }
}
}
bool afkShownThisSession, dailyShownThisSession;
void HookSystemEvents() { }
// ---------------- 채팅 (ChatSpawnController · LivestreamChatPresenter) ----------------
class PendingChat { public string text; public bool donate; public float age; }
readonly List<PendingChat> chatQueue = new List<PendingChat>();
readonly List<GameObject> chatLines = new List<GameObject>();
float chatSpawnTimer = 1f, chatShowTimer; int consecutiveDonate; bool lastShownDonate;
float viewTimer; BigInteger viewValue;
/// 상태 = 스트리머 상태(Idle 0 · Pose 1 · Hype 2) — GetCurrentChatSpawnState RVA 0x23FAF6C
int ChatState => G.HypePlaying ? 2 : (poseTimer > 0 ? 1 : 0);
void UpdateChat(float dt)
{
var rules = T.X.chatRules;
if (rules == null || rules.Count == 0 || chatContent == null) return;
var rule = rules.Find(r => r.state == ChatState) ?? rules[0];
chatSpawnTimer -= dt;
if (chatSpawnTimer <= 0f)
{
// GetSpawnInterval RVA 0x23FAEB0
float iv = (1f / Mathf.Max(0.01f, rule.CommentsPerSecond)) * Mathf.Pow(1f - Mathf.Clamp(rule.IntervalReducePercentPerRank, 0f, 0.99f), Mathf.Max(G.S.rankIndex, 0));
chatSpawnTimer = Mathf.Max(0.01f, iv);
// ShouldReplaceChatWithDonate RVA 0x2560AC8
bool donate;
if (consecutiveDonate >= rule.MaxConsecutiveDonate) { consecutiveDonate = 0; donate = false; }
else if (Random.value <= rule.DonateReplaceChance) { consecutiveDonate++; donate = true; }
else { consecutiveDonate = 0; donate = false; }
string name = T.ChatNames.Names[Random.Range(0, T.ChatNames.Names.Count)];
string txt = donate
? $"<color=#FFD24A>{name}</color> 님이 <color=#FFD24A>{Random.Range(T.X.chatDonateMin, T.X.chatDonateMax)}</color> 후원!"
: $"<color=#C9A8FF>{name}</color>: {T.Comments.Items[Random.Range(0, T.Comments.Items.Count)].CommentText}";
chatQueue.Add(new PendingChat { text = txt, donate = donate });
if (chatQueue.Count > T.X.chatQueueCap) chatQueue.RemoveAt(0);
}
for (int i = chatQueue.Count - 1; i >= 0; i--) { chatQueue[i].age += dt; if (chatQueue[i].age > T.X.chatMaxQueueAge) chatQueue.RemoveAt(i); }
chatShowTimer -= dt;
if (chatShowTimer <= 0f && chatQueue.Count > 0)
{
chatShowTimer = G.HypePlaying ? T.X.chatShowIntervalHype : T.X.chatShowInterval;
int pick = 0;
if (lastShownDonate) { int k = chatQueue.FindIndex(c => !c.donate); if (k >= 0) pick = k; } // PickNextPendingIndex RVA 0x25F5E74
var c0 = chatQueue[pick]; chatQueue.RemoveAt(pick);
lastShownDonate = c0.donate;
AddChatLine(c0.text, c0.donate);
}
// 시청자 수(FrameView.RandomView RVA 0x243A2DC) — 팔로워 × 90~119% · 5~120 s 마다
viewTimer -= dt;
if (viewTimer <= 0f || viewValue == 0)
{
viewTimer = Random.Range(T.X.viewRerollMin, T.X.viewRerollMax);
int pct = G.Follower <= T.X.viewSmallFollower ? Random.Range(T.X.viewSmallMin, T.X.viewRandomMax) : Random.Range(T.X.viewRandomMin, T.X.viewRandomMax);
viewValue = G.Follower * pct / 100;
}
if (viewText != null) viewText.text = Fmt.Num(viewValue);
}
void AddChatLine(string text, bool donate)
{
var bg = UI.Img(chatContent, "Chat", Skin.RoundSoft, donate ? new Color(0.45f, 0.28f, 0.05f, 0.8f) : new Color(0.12f, 0.14f, 0.24f, 0.65f));
var t = UI.Txt(bg.transform, "T", text, 30, Color.white, TextAnchor.MiddleLeft);
t.supportRichText = true; t.resizeTextForBestFit = false; t.horizontalOverflow = HorizontalWrapMode.Wrap; t.verticalOverflow = VerticalWrapMode.Overflow;
t.rectTransform.Fill(20, 6, 20, 6);
var le = bg.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = 56; le.minHeight = 56;
chatLines.Add(bg.gameObject);
while (chatLines.Count > Mathf.Max(1, T.X.chatVisibleLines)) { Kill(chatLines[0]); chatLines.RemoveAt(0); }
}
}
}