using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace TKS.OutGame { /// /// 뽑기 연출 표(Resources/UI/gacha_scene.csv · PD 2026-11-13 「삼고초려 — 초가집 앞 유비·관우·장비의 뒷모습 · 문이 덜컹거린 뒤 서서히 열릴 때 새어 나오는 빛으로 등급을 알게 · /// 흰(★) · 주황(★★) · 무지개(★★★)」). 형식 = 로비 장면 표(LobbyScene.Load) · 좌표 = 배경 그림 px(왼쪽 위 0,0 · 아래로 +) · 자리·크기·박자·색 = 표(C45) — 줄 뜻은 표 머리 주석. /// public static class GachaScene { public const string TablePath = "UI/gacha_scene"; static List rows; public static void Reload() { rows = null; } public static List Rows { get { if (rows == null) rows = LobbyScene.Load(TablePath); return rows; } } public static LobbyScene.Row First(string kind) { return DomesticScene.First(Rows, kind); } public static List All(string kind) { return DomesticScene.All(Rows, kind); } /// 등급 줄(tier1~3 · 없으면 아래 등급). public static LobbyScene.Row Tier(int tier) { for (int t = Mathf.Clamp(tier, 1, 3); t >= 1; t--) { var r = First("tier" + t); if (r != null) return r; } return null; } /// 등급별 박자 줄(timeN · 없으면 아래 등급 · 그래도 없으면 time) — PD 2026-11-13 「1성·2성은 문 열림이 길어질 필요 없이 바로 열려도 됨」 = 긴 기대감 연출은 ★★★ 만. public static LobbyScene.Row Time(int tier) { for (int t = Mathf.Clamp(tier, 1, 3); t >= 1; t--) { var r = First("time" + t); if (r != null) return r; } return First("time"); } /// 빛 그림(표 fx 줄 s_sprite 의 n 번째 · 0 광원 · 1 빛줄기 · 2 색 번짐 · 3 별 · 4 점 · 5 마당 빛 · 6 고리). public static Sprite Fx(int n) { var r = First("fx"); if (r == null) return null; var ids = r.Sprites; return n < ids.Length ? UISkin.Icon(ids[n]) : null; } /// 표와 배경 그림이 다 있는가(없으면 호출 쪽이 예전 결과 화면). public static bool Ready { get { var v = First("view"); return v != null && UISkin.Icon(v.sprite) != null && First("door") != null && Time(1) != null && First("rattle") != null && First("card") != null && Tier(1) != null; } } } /// /// 뽑기 연출 — ① 삼고초려 장면: 기다림 → 문이 덜컹(여러 번 · 사이에 뜸) → 문이 서서히 열리며 틈으로 등급 색 빛이 새어 나옴(기대감 · 여기서 등급을 알게 됨) → 활짝 열리며 빛이 터지고 삼형제가 반김/놀람 /// ② 카드 획득(PD 레퍼런스 = 카드가 UI 에 등장할 때): 위에서 떨어져 흰 번쩍 → 눌렸다 튀어 오름 → 먼지·반짝이 · 조각 게이지. /// 빛 = 부드러운 흰 그림을 덧셈 합성으로 겹침(가운데 흰 광원 + 길고 가는 빛줄기 + 색 번짐 + 별가루 입자 · PD 2026-11-13 「단순히 무지개색이 아니라 예시처럼 화려해 보이게 · 파티클」). /// 문이 활짝 열리면 영웅의 검은 실루엣만 문 안에 보인다(PD 2026-11-13 최종 · 제 색·이름은 카드에서) · ★·★★ 는 덜컹 한 번 뒤 바로 열림 · 긴 기대감 연출은 ★★★ 만(박자 = 표 timeN) · 장면 중 탭 = 건너뛰어 카드로(PD 「터치로 문이 열리는 장면을 스킵」). /// 만드는 법 = 내정 장면과 같게(배경 한 장 + 표 좌표 + 원화 프레임 교체 + 자리·배율 움직임) · 삼형제 = 이 연출 전용 그림(Resources/Units/gacha_* · 걸어서 · 무기 없음). /// 장면·카드는 매 프레임 바뀌므로 Frame 층 · 글자·버튼은 Event 층(P41). 시간 = 실시간(일시정지 무관). /// public class GachaCutscene : MonoBehaviour { public enum Step { Wait, Rattle, Leak, Open, Cards, Done } /// 캡처·테스트용: 0 보다 크면 매 프레임 이 초만큼만 흐른다(실시간 무시 · 프레임마다 같은 간격). public static float FixedDt = -1f; /// 캡처·테스트용: true 면 스스로 흐르지 않는다(호출 쪽이 Tick 으로 한 칸씩) · 실제 탭도 받지 않는다. public static bool Manual; const string IdleClip = "back_idle"; /// 덧셈 합성 셰이더(Resources · 없으면 보통 합성으로 — 빛이 덜 화려할 뿐 동작은 같음). public const string AdditiveShader = "UI/UiAdditive"; static Material addMat; /// 빛·별가루용 덧셈 합성 재질(한 장을 같이 씀 = 같은 아틀라스면 한 묶음). public static Material AddMat { get { if (addMat == null) { var sh = Resources.Load(AdditiveShader); if (sh != null) addMat = new Material(sh) { name = "GachaAdditive", hideFlags = HideFlags.HideAndDontSave }; } return addMat; } } class Actor { public RectTransform rt; public Image img; public UnitArtData data; public Sprite[] idle; public Sprite[] react; public Vector2 home; public float phase; public float flip; public RectTransform bang; public RectTransform shadow; public Vector2 shadowHome; public float idleOffset; } class Card { public RectTransform rt; public Image flash; public RectTransform deco; public RectTransform rays; public Image glow; public Image edge; public bool rainbow; public float amb; public RectTransform sheen; public float sheenW; public RectTransform strip; public RectTransform gauge; public float g0; public float g1; public float start; public Vector2 home; public bool landed; public int tier; } class Bit { public RectTransform rt; public Image img; public Vector2 pos; public Vector2 vel; public float grav; public float drag; public float t; public float life; public float s0; public float s1; public float a0; public float spin; public float tw; public float ph; public bool ring; public bool live; } class Ray { public RectTransform rt; public Image img; public LobbyScene.Row row; } OutGameSession s; List pulls; System.Action onClose; float mountedTop; LobbyScene.Row heroRow; RectTransform sil; Image silImg; Image veil; LobbyScene.Row timeRow, doorRow, rattleRow, zoomRow, tierRow, reactRow, sparkRow, ringRow, cardRow, puffRow, twinkleRow, flashRow, shadowRow, dimRow; readonly List tints = new List(); RectTransform clip, stage, backLayer, doorLayer, groundLayer, shadowLayer, fxLayer, bitLayer, frontLayer, cardsLayer, cardFx, ui; float artW, artH; RectTransform leafL, leafR, haloRt, coreRt, leakRt, lobeRoot; Image leafLImg, leafRImg, dimBack, glow, halo, slit, bloom, coreImg, leakImg, flash, shade, fade; Vector2 leafLHome, leafRHome; readonly List streaks = new List(); readonly List lobes = new List(); readonly List beams = new List(); readonly List actors = new List(); readonly List cards = new List(); readonly List bits = new List(); GameObject hint; float t; float tRattle, tOpen, tBurst, tReact, tFull, tEnd; float tCards = -1f; float flashA; float flashT = 99f; float ambAcc; int lastBeat = -1; bool burstDone, doneUi; float lastScale = -1f; Sprite coreSp, streakSp, lobeSp, starSp, dotSp, beamSp, ringSp, puffSp; public Step Stage { get; private set; } /// 문 빛 등급(뽑힌 것 중 가장 높은 등급 1~3). public int Tier { get; private set; } /// 문 안에 실루엣으로 서는 영웅(가장 높은 등급 · 새 영웅 우선). public GachaPull Featured { get; private set; } /// 검은 실루엣이 보이는가(문이 활짝 열린 뒤 · 테스트). public bool SilhouetteShown { get { return silImg != null && silImg.color.a > 0.15f; } } /// 실루엣 색(검정 · 제 색을 보이지 않음 · 테스트). public Color SilhouetteColor { get { return silImg != null ? silImg.color : Color.clear; } } /// 삼형제 반응 클립 이름(표 tier 줄 i_slot 0 = greet · 1 = shock). public string Reaction { get; private set; } public int ActorCount { get { return actors.Count; } } public int CardCount { get { return cards.Count; } } public float Elapsed { get { return t; } } /// 장면이 끝나 카드로 넘어가는 시각(캡처 길이). public float SceneEnd { get { return tEnd; } } /// 문 열림 비율(0 닫힘 ~ 1 다 열림). public float DoorOpen { get; private set; } /// 지금 문틈으로 새는 빛의 세기(0~1 · 테스트). public float LightLevel { get; private set; } /// 지금까지 덜컹거린 횟수(테스트). public int RattleCount { get { return lastBeat + 1; } } /// 빛줄기 수(표 streakN 줄 · 테스트). public int StreakCount { get { return streaks.Count; } } /// 지금 살아 있는 입자 수(별가루·고리·먼지 · 테스트). public int LiveBits { get { int n = 0; foreach (var b in bits) if (b.live) n++; return n; } } public int CardsLanded { get { int n = 0; foreach (var c in cards) if (c.landed) n++; return n; } } public static GachaCutscene Show(OutGameRoot root, OutGameSession s, List pulls, System.Action onClose) { var dim = UiKit.Fill("GachaResult", root.TopRoot); UiLayer.Nest(dim.gameObject, UiRate.Event, true); // 위 덮개 = 자기 캔버스(재화 바와 따로 · PD 2026-11-10) var c = dim.gameObject.AddComponent(); c.s = s; c.pulls = pulls; c.onClose = onClose; c.mountedTop = root.Battle != null && root.Battle.Config != null ? root.Battle.Config.mountedArtMaxTop : 0f; c.Build(dim); return c; } // ---------- 만들기 ---------- void Build(RectTransform dim) { doorRow = GachaScene.First("door"); rattleRow = GachaScene.First("rattle"); zoomRow = GachaScene.First("zoom"); sparkRow = GachaScene.First("spark"); ringRow = GachaScene.First("ring"); puffRow = GachaScene.First("puff"); twinkleRow = GachaScene.First("twinkle"); flashRow = GachaScene.First("flash"); shadowRow = GachaScene.First("shadow"); dimRow = GachaScene.First("dim"); cardRow = pulls.Count <= 1 && GachaScene.First("card1") != null ? GachaScene.First("card1") : GachaScene.First("card"); Tier = 1; Featured = null; foreach (var p in pulls) { Tier = Mathf.Max(Tier, Mathf.Clamp(p.tier, 1, 3)); if (Featured == null || p.tier > Featured.tier || (p.tier == Featured.tier && p.isNew && !Featured.isNew)) Featured = p; } tierRow = GachaScene.Tier(Tier); timeRow = GachaScene.Time(Tier); Reaction = tierRow.slot == 1 ? "shock" : "greet"; reactRow = GachaScene.First(Reaction); foreach (var r in GachaScene.All("tint" + Tier)) tints.Add(r.color); if (tints.Count == 0) tints.Add(tierRow.color); tRattle = timeRow.first + timeRow.stand; tOpen = tRattle + timeRow.ready; tBurst = tOpen + timeRow.hit * Mathf.Clamp01(timeRow.waves); // 서서히 열리는 구간 끝 = 활짝 열리기 시작 tReact = tBurst + timeRow.gapMin; tFull = tOpen + timeRow.hit; tEnd = tFull + timeRow.hold; coreSp = GachaScene.Fx(0); streakSp = GachaScene.Fx(1); lobeSp = GachaScene.Fx(2); starSp = GachaScene.Fx(3); dotSp = GachaScene.Fx(4); beamSp = GachaScene.Fx(5); ringSp = GachaScene.Fx(6); puffSp = puffRow != null ? UISkin.Icon(puffRow.sprite) : null; var back = UiKit.Paint(dim, GachaScene.First("view").color, false, true); // 그림 밖(다른 화면 비율) 바탕 + 탭 받는 판 var tap = dim.gameObject.AddComponent