386 lines
23 KiB
C#
386 lines
23 KiB
C#
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
using VS.Sim;
|
||
using Vector2 = UnityEngine.Vector2;
|
||
using Vector3 = UnityEngine.Vector3;
|
||
|
||
// 메인 화면 — 원작 GamePlayUI 배치(1080×1920): 상단 좌 프로필 · 상단 우 코인/초당/티켓 + 세로 버튼 · 좌 하이프 막대 · 하단 채팅 · 후원 받기 · 하단 5버튼
|
||
namespace VS.View
|
||
{
|
||
public partial class GameApp
|
||
{
|
||
RectTransform stage, roomRect;
|
||
Image roomBg, idolImg, idolShadow;
|
||
readonly Image[] props = new Image[11];
|
||
readonly int[] propStage = new int[11];
|
||
Text coinText, gpsText, ticketText, nameText, rankText, followerText, viewText, donateAmount, hypeLabel, boostText;
|
||
Image followerFill, hypeFill;
|
||
RectTransform hypeBar, chatContent, verticalButtons;
|
||
Button donateBtn, newHouseBtn, afkBtn;
|
||
Image donateDot;
|
||
GameObject hypeGlow;
|
||
int idolFrame; float idolTimer, blinkTimer, poseTimer;
|
||
|
||
// 방 소품 자리 (방 영역 정규 좌표 x, y, 크기) — 클론 배치(원작 아트 좌표 아님)
|
||
static readonly float[,] Slot =
|
||
{
|
||
{ 0.20f, 0.20f, 0.30f }, { 0.82f, 0.20f, 0.27f }, { 0.13f, 0.56f, 0.24f }, { 0.33f, 0.13f, 0.17f },
|
||
{ 0.64f, 0.30f, 0.32f }, { 0.66f, 0.47f, 0.20f }, { 0.88f, 0.45f, 0.19f }, { 0.50f, 0.06f, 0.48f },
|
||
{ 0.50f, 0.88f, 0.20f }, { 0.42f, 0.22f, 0.14f }, { 0f, 0f, 0f },
|
||
};
|
||
|
||
void BuildHud()
|
||
{
|
||
// 무대(방 + 스트리머) — 탭 영역
|
||
StreamRoot = UI.Rect("Stream", Root).Fill(0, TabH, 0, 0);
|
||
var tapArea = UI.Img(StreamRoot, "Stage", Art.Square, new Color(0, 0, 0, 0), true);
|
||
stage = tapArea.rectTransform.Fill(0, 0, 0, 0);
|
||
var et = tapArea.gameObject.AddComponent<EventTrigger>();
|
||
var down = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
|
||
down.callback.AddListener(e => OnStagePointerDown(((PointerEventData)e).position));
|
||
et.triggers.Add(down);
|
||
var up = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
|
||
up.callback.AddListener(e => OnStagePointerUp());
|
||
et.triggers.Add(up);
|
||
|
||
roomBg = UI.Img(stage, "Room", null, Color.white);
|
||
roomRect = roomBg.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 1), Vector2.zero, Vector2.zero);
|
||
roomRect.offsetMin = new Vector2(0, 560); roomRect.offsetMax = new Vector2(0, 0);
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
var im = UI.Img(roomRect, "Prop" + i, null, Color.white);
|
||
im.preserveAspect = true;
|
||
im.rectTransform.anchorMin = im.rectTransform.anchorMax = new Vector2(Slot[i, 0], Slot[i, 1]);
|
||
im.rectTransform.sizeDelta = new Vector2(1080 * Slot[i, 2], 1080 * Slot[i, 2]);
|
||
props[i] = im;
|
||
}
|
||
idolShadow = UI.Img(roomRect, "IdolShadow", Art.SoftCircle, new Color(0, 0, 0, 0.25f));
|
||
idolShadow.rectTransform.Place(new Vector2(0.5f, 0.1f), new Vector2(0.5f, 0.1f), Vector2.zero, new Vector2(420, 90));
|
||
idolImg = UI.Img(roomRect, "Idol", null, Color.white);
|
||
idolImg.preserveAspect = true;
|
||
idolImg.rectTransform.Place(new Vector2(0.5f, 0.1f), new Vector2(0.5f, 0.1f), new Vector2(0, 270), new Vector2(460, 644));
|
||
hypeGlow = UI.Img(roomRect, "HypeGlow", Art.SoftCircle, new Color(1f, 0.55f, 0.85f, 0.35f)).gameObject;
|
||
((RectTransform)hypeGlow.transform).Place(new Vector2(0.5f, 0.1f), new Vector2(0.5f, 0.1f), new Vector2(0, 330), new Vector2(900, 900));
|
||
hypeGlow.transform.SetSiblingIndex(idolImg.transform.GetSiblingIndex());
|
||
hypeGlow.SetActive(false);
|
||
|
||
var hud = UI.Rect("Hud", StreamRoot).Fill();
|
||
BuildTopLeft(hud);
|
||
BuildTopRight(hud);
|
||
BuildHypeBar(hud);
|
||
BuildBottom(hud);
|
||
BuildBattleScreen();
|
||
BuildTabs();
|
||
FxLayer = UI.Rect("Fx", Root).Fill();
|
||
PopupLayer = UI.Rect("Popups", Root).Fill();
|
||
RebuildStage();
|
||
}
|
||
|
||
void BuildTopLeft(RectTransform hud)
|
||
{
|
||
var card = UI.Img(hud, "StreamerProfile", Skin.Round, new Color(0.17f, 0.19f, 0.32f, 0.92f));
|
||
card.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(36, -40), new Vector2(560, 210), new Vector2(0, 1));
|
||
var av = UI.Img(card.transform, "Avatar", Skin.ProfileFrame, Pal.Pink);
|
||
av.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(105, 0), new Vector2(170, 170));
|
||
var avIn = UI.Img(av.transform, "Face", Skin.ProfileInner, Color.white);
|
||
avIn.rectTransform.Fill(12, 12, 12, 12);
|
||
var mask = avIn.gameObject.AddComponent<Mask>(); mask.showMaskGraphic = true;
|
||
var face = UI.Img(avIn.transform, "IdolFace", null, Color.white);
|
||
face.preserveAspect = true;
|
||
face.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, -150), new Vector2(330, 462));
|
||
avatarFace = face;
|
||
nameText = UI.Txt(card.transform, "Name", "", 42, Color.white, TextAnchor.MiddleLeft, true);
|
||
nameText.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(206, -40), new Vector2(-230, 56), new Vector2(0, 0.5f));
|
||
var rk = UI.Img(card.transform, "Rank", Skin.S("Label_Parallelogram_Purple") ?? Skin.Pill, Skin.On ? Color.white : Pal.Purple);
|
||
rk.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(206, 4), new Vector2(200, 50), new Vector2(0, 0.5f));
|
||
rankText = UI.Txt(rk.transform, "T", "", 30, Color.white); rankText.rectTransform.Fill(6, 2, 6, 2);
|
||
var bar = UI.Bar(card.transform, "BarFollower", new Color32(230, 232, 246, 255), Pal.Pink, out followerFill);
|
||
bar.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(206, 40), new Vector2(-236, 40), new Vector2(0, 0.5f));
|
||
followerText = UI.Txt(bar.transform, "T", "", 24, Color.white, TextAnchor.MiddleCenter, true); followerText.rectTransform.Fill(4, 0, 4, 0);
|
||
// FrameView (팔로워 아이콘 + 수)
|
||
var fv = UI.Img(hud, "FrameView", Skin.Counter, Skin.On ? Skin.CounterTint : new Color(0.15f, 0.1f, 0.25f, 0.65f));
|
||
fv.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(36, -266), new Vector2(300, 70), new Vector2(0, 1));
|
||
var fvIcon = UI.Icon(fv.transform, "Icon", Skin.Icon("view"), Color.white);
|
||
fvIcon.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(36, 0), new Vector2(64, 64));
|
||
viewText = UI.Txt(fv.transform, "T", "", 36, Color.white, TextAnchor.MiddleLeft); viewText.rectTransform.Fill(80, 0, 12, 0);
|
||
boostText = UI.Txt(hud, "Boosts", "", 30, Color.white, TextAnchor.UpperLeft, true);
|
||
boostText.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(40, -350), new Vector2(600, 170), new Vector2(0, 1));
|
||
}
|
||
|
||
Image avatarFace;
|
||
|
||
Text MakeCounter(RectTransform hud, string name, string icon, float y, out Image frame)
|
||
{
|
||
frame = UI.Img(hud, name, Skin.Counter, Skin.On ? Skin.CounterTint : new Color(0.15f, 0.1f, 0.25f, 0.7f));
|
||
frame.rectTransform.Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-190, y), new Vector2(330, 80), new Vector2(1, 1));
|
||
var ic = UI.Icon(frame.transform, "Icon", Skin.Icon(icon), Color.white);
|
||
ic.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(30, 0), new Vector2(84, 84));
|
||
var t = UI.Txt(frame.transform, "T", "0", 40, Color.white, TextAnchor.MiddleRight); t.rectTransform.Fill(80, 0, 22, 0);
|
||
return t;
|
||
}
|
||
|
||
void BuildTopRight(RectTransform hud)
|
||
{
|
||
coinText = MakeCounter(hud, "FrameCoin", "coin", -40, out _);
|
||
gpsText = MakeCounter(hud, "FrameCoin/s", "coin", -130, out _);
|
||
ticketText = MakeCounter(hud, "FrameTicket", "ticket", -220, out var tf);
|
||
var tb = tf.gameObject.AddComponent<Button>(); tb.onClick.AddListener(() => OpenTicketShop());
|
||
tf.raycastTarget = true;
|
||
if (Skin.On) // 티켓 칸 오른쪽 「+」 (상점)
|
||
{
|
||
var plus = UI.Img(tf.transform, "Plus", Skin.CounterBtn, Color.white);
|
||
plus.rectTransform.Place(new Vector2(1, 0.5f), new Vector2(1, 0.5f), new Vector2(-4, 2), new Vector2(66, 68));
|
||
UI.Icon(plus.transform, "I", Skin.Plus, Color.white).rectTransform.Fill(18, 22, 18, 16);
|
||
ticketText.rectTransform.Fill(80, 0, 80, 0);
|
||
}
|
||
verticalButtons = UI.Rect("GirdButtonsVertical", hud).Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-24, -40), new Vector2(140, 1400), new Vector2(1, 1));
|
||
var vl = verticalButtons.gameObject.AddComponent<VerticalLayoutGroup>();
|
||
vl.spacing = 22; vl.childAlignment = TextAnchor.UpperCenter; vl.childControlHeight = false; vl.childControlWidth = false; vl.childForceExpandHeight = false;
|
||
SideBtn("ButtonSetting", "gear", () => OpenSettings());
|
||
afkBtn = SideBtn("ButtonClaimAfk", "afk", () => OpenAfkPopup(false));
|
||
newHouseBtn = SideBtn("ButtonNewHouse", "house", () => OpenBuyNewHouse());
|
||
SideBtn("ButtonTicketShop", "shop", () => OpenTicketShop());
|
||
BuildGiftSlots(verticalButtons);
|
||
}
|
||
|
||
Button SideBtn(string name, string icon, UnityEngine.Events.UnityAction a)
|
||
{
|
||
var b = UI.Btn(verticalButtons, name, Skin.Dark, Color.white, a);
|
||
b.GetComponent<RectTransform>().sizeDelta = new Vector2(128, 128);
|
||
var ic = UI.Icon(b.transform, "Icon", Skin.Icon(icon), Color.white);
|
||
ic.rectTransform.Fill(16, 20, 16, 14);
|
||
return b;
|
||
}
|
||
|
||
void BuildHypeBar(RectTransform hud)
|
||
{
|
||
var bg = UI.Img(hud, "BarHype", Skin.Round, new Color(0.17f, 0.19f, 0.32f, 0.85f));
|
||
hypeBar = bg.rectTransform.Place(new Vector2(0, 0.5f), new Vector2(0, 0.5f), new Vector2(70, 120), new Vector2(84, 560));
|
||
hypeFill = UI.Img(bg.transform, "FillHype", Skin.On ? Skin.RoundSoft : Skin.Pill, Pal.Pink);
|
||
hypeFill.type = Image.Type.Filled; hypeFill.fillMethod = Image.FillMethod.Vertical; hypeFill.fillOrigin = 0;
|
||
hypeFill.rectTransform.Fill(12, 12, 12, 12);
|
||
var ic = UI.Icon(bg.transform, "Fire", Skin.Icon("fire"), Color.white);
|
||
ic.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, 30), new Vector2(110, 110));
|
||
hypeLabel = UI.Txt(bg.transform, "Label", "", 26, Color.white, TextAnchor.MiddleCenter, true);
|
||
hypeLabel.rectTransform.Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, -36), new Vector2(160, 50));
|
||
}
|
||
|
||
void BuildBottom(RectTransform hud)
|
||
{
|
||
var frame = UI.Rect("FrameBanner", hud).Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 0), new Vector2(0, 560), new Vector2(0.5f, 0));
|
||
var bgb = UI.Img(frame, "Bg", Skin.BottomBar, Skin.On ? new Color(1f, 1f, 1f, 0.96f) : new Color(0.16f, 0.1f, 0.26f, 0.92f));
|
||
bgb.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), Vector2.zero, new Vector2(0, 190), new Vector2(0.5f, 0));
|
||
var btns = UI.Rect("FrameButtons", frame).Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 20), new Vector2(-40, 150), new Vector2(0.5f, 0));
|
||
var hl = btns.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.spacing = 18; hl.childAlignment = TextAnchor.MiddleCenter; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = true;
|
||
BottomBtn(btns, "ButtonCloset", "closet", L("UI.Closet.Title", "의상"), Pal.Pink, () => OpenCloset());
|
||
BottomBtn(btns, "ButtonHouse", "house", L("UI.House.Title", "방"), Pal.Orange, () => OpenHouse());
|
||
BottomBtn(btns, "ButtonSkill", "skill", L("UI.Skill.Title", "스킬"), Pal.Purple, () => OpenSkill());
|
||
BottomBtn(btns, "ButtonBoosterHeart", "x2", "x2", Pal.Red, () => OpenBoosterX2());
|
||
BottomBtn(btns, "ButtonBoosterAuto", "auto", L("Gameplay.Booster.AutoClick.Title", "자동"), Pal.Blue, () => OpenBoosterAuto());
|
||
// 채팅
|
||
var chat = UI.Rect("BarChat", frame).Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(-60, 200), new Vector2(-300, 340), new Vector2(0.5f, 0));
|
||
chat.gameObject.AddComponent<RectMask2D>();
|
||
chatContent = UI.Rect("ChatContent", chat);
|
||
chatContent.anchorMin = new Vector2(0, 0); chatContent.anchorMax = new Vector2(1, 0); chatContent.pivot = new Vector2(0.5f, 0);
|
||
chatContent.anchoredPosition = Vector2.zero; chatContent.sizeDelta = new Vector2(0, 340);
|
||
var vl = chatContent.gameObject.AddComponent<VerticalLayoutGroup>();
|
||
vl.childAlignment = TextAnchor.LowerLeft; vl.spacing = 6; vl.childControlHeight = true; vl.childControlWidth = true; vl.childForceExpandHeight = false;
|
||
// 후원 받기 · 퀘스트
|
||
donateBtn = UI.TextBtn(frame, "ButtonClaimDonate", "", Pal.Green, 36, () => OpenClaimDonate(), out donateAmount);
|
||
donateBtn.GetComponent<RectTransform>().Place(new Vector2(1, 0), new Vector2(1, 0), new Vector2(-150, 300), new Vector2(260, 150));
|
||
donateAmount.rectTransform.Fill(14, 10, 14, 44);
|
||
var lbl = UI.Txt(donateBtn.transform, "Title", L("UI.Gameplay.ClaimDonate", "후원 받기"), 30, Color.white, TextAnchor.MiddleCenter, true);
|
||
lbl.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -26), new Vector2(-10, 44));
|
||
donateDot = UI.Dot(donateBtn.transform, "DotRed");
|
||
var task = UI.Btn(frame, "ButtonTask", Skin.Dark, Color.white, () => OpenTasks());
|
||
task.GetComponent<RectTransform>().Place(new Vector2(1, 0), new Vector2(1, 0), new Vector2(-150, 470), new Vector2(128, 128));
|
||
UI.Icon(task.transform, "Icon", Skin.Icon("task"), Color.white).rectTransform.Fill(16, 20, 16, 14);
|
||
taskDot = UI.Dot(task.transform, "DotRed", 34);
|
||
}
|
||
|
||
Image taskDot;
|
||
|
||
void BottomBtn(RectTransform parent, string name, string icon, string label, Color c, UnityEngine.Events.UnityAction a)
|
||
{
|
||
var sp = Skin.Button(c);
|
||
var b = UI.Btn(parent, name, sp ?? Skin.Round, sp != null ? Color.white : c, a);
|
||
UI.Icon(b.transform, "Icon", Skin.Icon(icon), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -48), new Vector2(84, 84));
|
||
var t = UI.Txt(b.transform, "Label", label, 28, Color.white, TextAnchor.MiddleCenter, true);
|
||
t.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 32), new Vector2(-8, 40));
|
||
}
|
||
|
||
public string L(string key, string fallback) => T.Loc.Has(key) ? T.Loc.Get(key) : fallback;
|
||
|
||
public string ItemName(int house, ItemRow it) => T.Loc.Get($"Content.Room.House{house + 1}.{it.Index}.Name", it.Name);
|
||
public string SkillName(ItemRow it) => T.Loc.Get($"Content.Skill.{it.Index}.Name", T.Loc.Get("Content.Skill." + it.Name.Replace(" Skill", "") + ".Name", it.Name));
|
||
|
||
// ---------------- 무대 갱신 ----------------
|
||
public void RebuildStage()
|
||
{
|
||
for (int i = 0; i < 11; i++) propStage[i] = -99;
|
||
RefreshStage();
|
||
SetIdolFrame(idolFrame);
|
||
}
|
||
|
||
/// RoomVisualController.GetVisualIndex RVA 0x24C6C80 (문턱 24 · 49 = .cctor RVA 0x24C7904 · 최대 레벨 = 3)
|
||
public static int VisualIndex(int level, int maxLevel)
|
||
{
|
||
if (level < 0) return -1;
|
||
if (maxLevel >= 0 && level >= maxLevel) return 3;
|
||
if (level < 49) return level >= 24 ? 1 : 0;
|
||
return 2;
|
||
}
|
||
|
||
void RefreshStage()
|
||
{
|
||
int h = G.S.house;
|
||
var room = G.Room;
|
||
int roomStage = -1;
|
||
var roomItem = G.RoomItem(10);
|
||
if (roomItem != null) roomStage = VisualIndex(G.EffRoomLevel(10), roomItem.Levels.Count - 1);
|
||
if (propStage[10] != roomStage) { propStage[10] = roomStage; roomBg.sprite = Art.RoomBg(h, roomStage); }
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
var it = room.Items.Find(x => x.Index == i);
|
||
int st = it == null ? -1 : VisualIndex(G.EffRoomLevel(i), it.Levels.Count - 1);
|
||
if (propStage[i] == st) continue;
|
||
bool grew = propStage[i] != -99 && st > propStage[i];
|
||
propStage[i] = st;
|
||
props[i].gameObject.SetActive(st >= 0);
|
||
if (st >= 0) props[i].sprite = Art.Prop(h, i, it.Name, st);
|
||
if (grew) Pop(props[i].rectTransform);
|
||
}
|
||
}
|
||
|
||
readonly List<(RectTransform rt, float t)> pops = new List<(RectTransform, float)>();
|
||
void Pop(RectTransform rt) { pops.Add((rt, 0f)); }
|
||
|
||
// ---------------- 갱신 ----------------
|
||
public void RefreshAll()
|
||
{
|
||
if (G == null || coinText == null) return;
|
||
coinText.text = Fmt.Num(G.Coin);
|
||
gpsText.text = Fmt.Num(G.PassGps()) + "/s";
|
||
ticketText.text = G.S.ticket.ToString();
|
||
nameText.text = G.S.idolName;
|
||
var rk = G.CurrentRank();
|
||
rankText.text = rk != null ? T.Loc.Get($"Content.Rank.{rk.Index}.Name", rk.Rank) : "";
|
||
var next = G.NextRank();
|
||
if (next != null)
|
||
{
|
||
var cur = rk != null ? Big.P(rk.Follower) : BigInteger.Zero;
|
||
var nx = Big.P(next.Follower);
|
||
double f = nx > cur ? (double)(G.Follower - cur) / (double)(nx - cur) : 1;
|
||
followerFill.fillAmount = Mathf.Clamp01((float)f);
|
||
followerText.text = $"{Fmt.Num(G.Follower)} / {Fmt.Num(nx)}";
|
||
}
|
||
else { followerFill.fillAmount = 1; followerText.text = Fmt.Num(G.Follower); }
|
||
var claim = G.DonateClaimable;
|
||
donateAmount.text = Fmt.Num(claim);
|
||
donateBtn.interactable = claim > 0;
|
||
donateDot.gameObject.SetActive(claim > 0);
|
||
hypeFill.fillAmount = G.Hype;
|
||
hypeLabel.text = G.HypePlaying ? "HYPE x" + T.Cfg.remote.HypeMulti.ToString("0.#") : Mathf.RoundToInt(G.Hype * 100) + "%";
|
||
newHouseBtn.gameObject.SetActive(G.HasNextHouse && G.NextHouseRankReq() <= G.S.rankIndex);
|
||
RefreshSystemsHud();
|
||
RefreshStage();
|
||
RefreshOpenPopup();
|
||
if (G.RankUpPending && !PopupOpen && !Headless) OpenRankUp();
|
||
TryOpenStory();
|
||
}
|
||
|
||
void SetIdolFrame(int f)
|
||
{
|
||
int outfit = G.S.closetCurrent;
|
||
if (PreviewOutfit >= 0) outfit = PreviewOutfit;
|
||
idolImg.sprite = Art.Idol(outfit, f);
|
||
if (avatarFace != null) avatarFace.sprite = Art.Idol(outfit, 0);
|
||
}
|
||
|
||
public int PreviewOutfit = -1;
|
||
|
||
// ---------------- 연출 ----------------
|
||
void UpdateHudFx(float dt)
|
||
{
|
||
// 스트리머 프레임 (원작 IdolController: 대기 깜빡임 2 s/0.1 s · 하이프 프레임 0.5 s · 포즈 0.6 s)
|
||
var ic = T.Cfg.idol;
|
||
if (G.HypePlaying)
|
||
{
|
||
idolTimer += dt;
|
||
if (idolTimer >= ic._timeBetweenHypeFrame) { idolTimer = 0; idolFrame = idolFrame >= 3 && idolFrame < 6 ? idolFrame + 1 : 3; }
|
||
}
|
||
else if (poseTimer > 0) { poseTimer -= dt; idolFrame = 2; }
|
||
else
|
||
{
|
||
blinkTimer += dt;
|
||
idolFrame = blinkTimer > ic._timeBetweenIdleBlink ? 1 : 0;
|
||
if (blinkTimer > ic._timeBetweenIdleBlink + ic._idleBlinkDuration) blinkTimer = 0;
|
||
}
|
||
SetIdolFrame(idolFrame);
|
||
hypeGlow.SetActive(G.HypePlaying);
|
||
if (G.HypePlaying) hypeGlow.transform.localScale = Vector3.one * (1f + 0.06f * Mathf.Sin(Time.unscaledTime * 8f));
|
||
// 탭 펀치(원작 InputHandler 스프링: 강도 0.07 · 강성 180 · 감쇠 12)
|
||
var inp = T.Cfg.input;
|
||
float acc = -inp._punchSpringStiffness * punch - inp._punchSpringDamping * punchVel;
|
||
punchVel += acc * dt; punch += punchVel * dt;
|
||
idolImg.rectTransform.localScale = new Vector3(1f + punch, 1f - punch * 0.6f, 1f);
|
||
for (int i = pops.Count - 1; i >= 0; i--)
|
||
{
|
||
var (rt, t) = pops[i];
|
||
t += dt;
|
||
if (rt == null || t > 0.4f) { if (rt != null) rt.localScale = Vector3.one; pops.RemoveAt(i); continue; }
|
||
rt.localScale = Vector3.one * (1f + 0.25f * Mathf.Sin(t / 0.4f * Mathf.PI));
|
||
pops[i] = (rt, t);
|
||
}
|
||
for (int i = floats.Count - 1; i >= 0; i--)
|
||
{
|
||
var fl = floats[i];
|
||
fl.t += dt;
|
||
if (fl.t > fl.life) { Kill(fl.rt.gameObject); floats.RemoveAt(i); continue; }
|
||
fl.rt.anchoredPosition += fl.vel * dt;
|
||
var g = fl.rt.GetComponent<CanvasGroup>(); g.alpha = 1f - fl.t / fl.life;
|
||
floats[i] = fl;
|
||
}
|
||
UpdateChat(dt);
|
||
}
|
||
|
||
float punch, punchVel;
|
||
|
||
class Floater { public RectTransform rt; public Vector2 vel; public float t, life; }
|
||
readonly List<Floater> floats = new List<Floater>();
|
||
|
||
public void FloatText(Vector2 local, string s, Color c, int size = 48, Sprite icon = null, bool box = false)
|
||
{
|
||
var rt = UI.Rect("Float", FxLayer);
|
||
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = local; rt.sizeDelta = new Vector2(box ? 820 : 420, 90);
|
||
rt.gameObject.AddComponent<CanvasGroup>().blocksRaycasts = false;
|
||
if (box) UI.Img(rt, "Bg", Skin.Toast, new Color(0.15f, 0.17f, 0.3f, 0.88f)).rectTransform.Fill(0, -8, 0, -8);
|
||
var t = UI.Txt(rt, "T", s, size, c, TextAnchor.MiddleCenter, true); t.rectTransform.Fill();
|
||
if (icon != null) UI.Img(rt, "I", icon, Color.white).rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-150, 0), new Vector2(64, 64));
|
||
floats.Add(new Floater { rt = rt, vel = new Vector2(Random.Range(-40f, 40f), 260f), life = 1.1f });
|
||
}
|
||
|
||
Vector2 ScreenToRoot(Vector2 screen)
|
||
{
|
||
RectTransformUtility.ScreenPointToLocalPointInRectangle(FxLayer, screen, Cam, out var lp);
|
||
return lp;
|
||
}
|
||
|
||
void OnTapVisual(Vector2 screenPos, bool coin)
|
||
{
|
||
var lp = ScreenToRoot(screenPos);
|
||
var h = UI.Icon(FxLayer, "Heart", Skin.Icon("heart"), Color.white);
|
||
h.rectTransform.anchorMin = h.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||
h.rectTransform.anchoredPosition = lp; h.rectTransform.sizeDelta = new Vector2(90, 90);
|
||
h.gameObject.AddComponent<CanvasGroup>().blocksRaycasts = false;
|
||
floats.Add(new Floater { rt = h.rectTransform, vel = new Vector2(Random.Range(-80f, 80f), 320f), life = 0.8f });
|
||
if (coin) { punchVel += T.Cfg.input._punchTapForce * T.Cfg.input._punchScaleIntensity * 10f; poseTimer = T.Cfg.input._returnIdleDelay; }
|
||
}
|
||
}
|
||
}
|