2026-11-13 11:29:46 +00:00
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.UI ;
namespace TKS.OutGame
{
/// <summary>
/// 뽑기 연출 표(Resources/UI/gacha_scene.csv · PD 2026-11-13 「삼고초려 — 초가집 앞 유비·관우·장비의 뒷모습 · 문이 덜컹거린 뒤 서서히 열릴 때 새어 나오는 빛으로 등급을 알게 ·
/// 흰(★) · 주황(★★) · 무지개(★★★)」). 형식 = 로비 장면 표(LobbyScene.Load) · 좌표 = 배경 그림 px(왼쪽 위 0,0 · 아래로 +) · 자리·크기·박자·색 = 표(C45) — 줄 뜻은 표 머리 주석.
/// </summary>
public static class GachaScene
{
public const string TablePath = "UI/gacha_scene" ;
static List < LobbyScene . Row > rows ;
public static void Reload ( )
{
rows = null ;
}
public static List < LobbyScene . Row > 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 < LobbyScene . Row > All ( string kind )
{
return DomesticScene . All ( Rows , kind ) ;
}
/// <summary>등급 줄(tier1~3 · 없으면 아래 등급).</summary>
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 ;
}
/// <summary>등급별 박자 줄(timeN · 없으면 아래 등급 · 그래도 없으면 time) — PD 2026-11-13 「1성·2성은 문 열림이 길어질 필요 없이 바로 열려도 됨」 = 긴 기대감 연출은 ★★★ 만.</summary>
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" ) ;
}
/// <summary>빛 그림(표 fx 줄 s_sprite 의 n 번째 · 0 광원 · 1 빛줄기 · 2 색 번짐 · 3 별 · 4 점 · 5 마당 빛 · 6 고리).</summary>
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 ;
}
/// <summary>표와 배경 그림이 다 있는가(없으면 호출 쪽이 예전 결과 화면).</summary>
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 ;
}
}
}
/// <summary>
/// 뽑기 연출 — ① 삼고초려 장면: 기다림 → 문이 덜컹(여러 번 · 사이에 뜸) → 문이 서서히 열리며 틈으로 등급 색 빛이 새어 나옴(기대감 · 여기서 등급을 알게 됨) → 활짝 열리며 빛이 터지고 삼형제가 반김/놀람
/// ② 카드 획득(PD 레퍼런스 = 카드가 UI 에 등장할 때): 위에서 떨어져 흰 번쩍 → 눌렸다 튀어 오름 → 먼지·반짝이 · 조각 게이지.
/// 빛 = 부드러운 흰 그림을 덧셈 합성으로 겹침(가운데 흰 광원 + 길고 가는 빛줄기 + 색 번짐 + 별가루 입자 · PD 2026-11-13 「단순히 무지개색이 아니라 예시처럼 화려해 보이게 · 파티클」).
/// 문이 활짝 열리면 영웅의 검은 실루엣만 문 안에 보인다(PD 2026-11-13 최종 · 제 색·이름은 카드에서) · ★·★★ 는 덜컹 한 번 뒤 바로 열림 · 긴 기대감 연출은 ★★★ 만(박자 = 표 timeN) · 장면 중 탭 = 건너뛰어 카드로(PD 「터치로 문이 열리는 장면을 스킵」).
/// 만드는 법 = 내정 장면과 같게(배경 한 장 + 표 좌표 + 원화 프레임 교체 + 자리·배율 움직임) · 삼형제 = 이 연출 전용 그림(Resources/Units/gacha_* · 걸어서 · 무기 없음).
/// 장면·카드는 매 프레임 바뀌므로 Frame 층 · 글자·버튼은 Event 층(P41). 시간 = 실시간(일시정지 무관).
/// </summary>
public class GachaCutscene : MonoBehaviour
{
public enum Step
{
Wait ,
Rattle ,
Leak ,
Open ,
Cards ,
Done
}
/// <summary>캡처·테스트용: 0 보다 크면 매 프레임 이 초만큼만 흐른다(실시간 무시 · 프레임마다 같은 간격).</summary>
public static float FixedDt = - 1f ;
/// <summary>캡처·테스트용: true 면 스스로 흐르지 않는다(호출 쪽이 Tick 으로 한 칸씩) · 실제 탭도 받지 않는다.</summary>
public static bool Manual ;
const string IdleClip = "back_idle" ;
/// <summary>덧셈 합성 셰이더(Resources · 없으면 보통 합성으로 — 빛이 덜 화려할 뿐 동작은 같음).</summary>
public const string AdditiveShader = "UI/UiAdditive" ;
static Material addMat ;
/// <summary>빛·별가루용 덧셈 합성 재질(한 장을 같이 씀 = 같은 아틀라스면 한 묶음).</summary>
public static Material AddMat
{
get
{
if ( addMat = = null )
{
var sh = Resources . Load < Shader > ( 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 < GachaPull > 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 < Color > tints = new List < Color > ( ) ;
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 < Ray > streaks = new List < Ray > ( ) ;
readonly List < Ray > lobes = new List < Ray > ( ) ;
readonly List < Ray > beams = new List < Ray > ( ) ;
readonly List < Actor > actors = new List < Actor > ( ) ;
readonly List < Card > cards = new List < Card > ( ) ;
readonly List < Bit > bits = new List < Bit > ( ) ;
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 ; }
/// <summary>문 빛 등급(뽑힌 것 중 가장 높은 등급 1~3).</summary>
public int Tier { get ; private set ; }
/// <summary>문 안에 실루엣으로 서는 영웅(가장 높은 등급 · 새 영웅 우선).</summary>
public GachaPull Featured { get ; private set ; }
/// <summary>검은 실루엣이 보이는가(문이 활짝 열린 뒤 · 테스트).</summary>
public bool SilhouetteShown { get { return silImg ! = null & & silImg . color . a > 0.15f ; } }
/// <summary>실루엣 색(검정 · 제 색을 보이지 않음 · 테스트).</summary>
public Color SilhouetteColor { get { return silImg ! = null ? silImg . color : Color . clear ; } }
/// <summary>삼형제 반응 클립 이름(표 tier 줄 i_slot 0 = greet · 1 = shock).</summary>
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 ; } }
/// <summary>장면이 끝나 카드로 넘어가는 시각(캡처 길이).</summary>
public float SceneEnd { get { return tEnd ; } }
/// <summary>문 열림 비율(0 닫힘 ~ 1 다 열림).</summary>
public float DoorOpen { get ; private set ; }
/// <summary>지금 문틈으로 새는 빛의 세기(0~1 · 테스트).</summary>
public float LightLevel { get ; private set ; }
/// <summary>지금까지 덜컹거린 횟수(테스트).</summary>
public int RattleCount { get { return lastBeat + 1 ; } }
/// <summary>빛줄기 수(표 streakN 줄 · 테스트).</summary>
public int StreakCount { get { return streaks . Count ; } }
/// <summary>지금 살아 있는 입자 수(별가루·고리·먼지 · 테스트).</summary>
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 < GachaPull > 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 < GachaCutscene > ( ) ;
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 < Button > ( ) ;
tap . transition = Selectable . Transition . None ;
tap . targetGraphic = back ;
tap . onClick . AddListener ( OnPointer ) ;
BuildStage ( dim ) ;
flash = Full ( "Flash" , dim , Color . white ) ;
SetA ( flash , 0f ) ;
fade = Full ( "Fade" , dim , Color . black ) ;
shade = Full ( "Shade" , dim , dimRow ! = null ? dimRow . color : new Color ( 0f , 0f , 0f , 0.85f ) ) ;
shade . gameObject . SetActive ( false ) ;
cardsLayer = UiLayer . Group ( "Cards" , dim , UiRate . Frame ) ;
cardFx = UiKit . Fill ( "CardFx" , cardsLayer ) ;
ui = UiKit . Fill ( "UI" , dim ) ;
hint = Hint ( "화면을 터치하면 건너뜁니다" , 34 ) ;
Apply ( 0f ) ;
FitStage ( ) ;
}
static Image Full ( string name , Transform parent , Color c )
{
var img = UiKit . Paint ( UiKit . Fill ( name , parent ) , c , false , false ) ;
img . raycastTarget = false ;
return img ;
}
static void SetA ( Graphic g , float a )
{
if ( g = = null ) return ;
var c = g . color ;
if ( Mathf . Approximately ( c . a , a ) ) return ;
c . a = a ;
g . color = c ;
}
static void SetRgb ( Graphic g , Color rgb )
{
if ( g = = null ) return ;
var c = g . color ;
rgb . a = c . a ;
if ( c ! = rgb ) g . color = rgb ;
}
RectTransform Px ( string name , Transform parent , float x , float y , float w , float h , float px = 0.5f , float py = 0.5f )
{
return UiKit . Rect ( name , parent , new Vector2 ( 0f , 1f ) , new Vector2 ( px , py ) , new Vector2 ( x , - y ) , new Vector2 ( w , h ) ) ;
}
Image Spr ( string name , Transform parent , Sprite sp , float x , float y , float w , float h , float px = 0.5f , float py = 0.5f )
{
var img = Px ( name , parent , x , y , w , h , px , py ) . gameObject . AddComponent < Image > ( ) ;
img . sprite = sp ;
img . raycastTarget = false ;
return img ;
}
/// <summary>빛 그림 한 장(덧셈 합성 · 처음엔 안 보임).</summary>
Image Glow ( string name , Transform parent , Sprite sp , float x , float y , float w , float h , Color c , float px = 0.5f , float py = 0.5f )
{
var img = Spr ( name , parent , sp , x , y , w , h , px , py ) ;
c . a = 0f ;
img . color = c ;
var m = AddMat ;
if ( m ! = null ) img . material = m ;
img . enabled = sp ! = null ;
return img ;
}
void BuildStage ( RectTransform dim )
{
var view = GachaScene . First ( "view" ) ;
var bg = UISkin . Icon ( view . sprite ) ;
artW = view . w > 0f ? view . w : bg . rect . width ;
artH = view . h > 0f ? view . h : bg . rect . height ;
clip = UiKit . Fill ( "Clip" , dim ) ;
clip . gameObject . AddComponent < RectMask2D > ( ) ;
stage = UiKit . Rect ( "Stage" , clip , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( artW , artH ) ) ;
UiLayer . Nest ( stage . gameObject , UiRate . Frame , false ) ; // 장면 전체가 매 프레임 움직임(다가가는 화면 · 빛 · 문 · 사람) — 한 장으로
var art = UiKit . Fill ( "Art" , stage ) . gameObject . AddComponent < Image > ( ) ;
art . sprite = bg ;
art . raycastTarget = false ;
backLayer = UiKit . Fill ( "Back" , stage ) ;
doorLayer = UiKit . Fill ( "Door" , stage ) ;
groundLayer = UiKit . Fill ( "Ground" , stage ) ;
shadowLayer = UiKit . Fill ( "Shadows" , stage ) ;
fxLayer = UiKit . Fill ( "Fx" , stage ) ;
frontLayer = UiKit . Fill ( "Front" , stage ) ;
float dx = doorRow . x , dy = doorRow . y , dw = doorRow . w , dh = doorRow . h ;
float cx = dx + dw * 0.5f , cy = dy + dh * 0.5f ;
var white = Color . white ;
var light = tierRow . n = = 1 ? white : Color . Lerp ( tierRow . color , white , 0.55f ) ; // 가운데 광원 = 등급 색을 흰 쪽으로(빛은 가운데가 흼)
var edgeLight = tierRow . n = = 1 ? white : Color . Lerp ( tierRow . color , white , 0.2f ) ; // 틈·세로 빛줄기 = 등급 색이 또렷하게(새는 빛으로 등급을 알게)
var insRow = GachaScene . First ( "inside" + Tier ) ;
var inside = insRow ! = null ? insRow . color : light ; // 문 안 판 = 등급 색(그 위에 흰 광원이 얹혀 가운데 흰 빛 → 바깥 등급색으로 이어짐 · 흰 네모로 안 보이게)
// 문 뒤: 둘레 어둠 → 문 안(어두운 방) → 문 안 빛(틈으로 보임 · 문틀에서 조금 들임) → 문 둘레 번짐
dimBack = Full ( "DimBack" , backLayer , new Color ( 0.03f , 0.04f , 0.09f , 0f ) ) ; // 둘레를 어둡게 = 빛이 도드라짐(덜컹거릴 때부터 서서히)
UiKit . Paint ( Px ( "Inside" , backLayer , dx , dy , dw , dh , 0f , 1f ) , doorRow . color , false , false ) ;
float inset = doorRow . first ;
glow = UiKit . Paint ( Px ( "Glow" , backLayer , dx + inset , dy + inset , dw - inset * 2f , dh - inset , 0f , 1f ) , inside , false , false ) ;
float hsz = Mathf . Max ( dw , dh ) * tierRow . waves ;
halo = Glow ( "Halo" , backLayer , coreSp , cx , cy , hsz , hsz , tierRow . color ) ;
haloRt = halo . rectTransform ;
// 문짝 2(배경에서 오린 그림 · 바깥 끝 = 경첩 · 폭이 줄며 열림 = 가운데 틈이 벌어짐) + 문틈 빛 번짐
var leafIds = doorRow . Sprites ;
leafLImg = Spr ( "LeafL" , doorLayer , leafIds . Length > 0 ? UISkin . Icon ( leafIds [ 0 ] ) : null , dx , dy , dw * 0.5f , dh , 0f , 1f ) ;
leafRImg = Spr ( "LeafR" , doorLayer , leafIds . Length > 1 ? UISkin . Icon ( leafIds [ 1 ] ) : null , dx + dw , dy , dw * 0.5f , dh , 1f , 1f ) ;
leafL = leafLImg . rectTransform ;
leafR = leafRImg . rectTransform ;
leafLHome = leafL . anchoredPosition ;
leafRHome = leafR . anchoredPosition ;
slit = Glow ( "Slit" , doorLayer , coreSp , cx , cy , doorRow . gapMin , dh * doorRow . gapMax , edgeLight ) ;
// 문틈에서 마당으로 쏟아지는 빛(부드러운 부채 · 줄마다 색·기울기 = 여러 색이 겹쳐 가운데가 흼)
foreach ( var r in GachaScene . All ( "beam" + Tier ) )
{
var bi = Glow ( "Beam" + beams . Count , groundLayer , beamSp , cx , dy + dh , dw * doorRow . max , tierRow . h , r . color , 0.5f , 1f ) ;
bi . rectTransform . localEulerAngles = new Vector3 ( 0f , 0f , r . amp ) ;
beams . Add ( new Ray { rt = bi . rectTransform , img = bi , row = r } ) ;
}
// 사람 뒤 빛 층: 색 번짐(천천히 돎) → 가운데 흰 광원 → 새는 동안의 세로 빛줄기 → 길고 가는 빛줄기들 → 입자
var lightRoot = UiKit . Fill ( "Light" , fxLayer ) ;
lobeRoot = Px ( "Lobes" , lightRoot , cx , cy , 0f , 0f ) ;
var mid = new Vector2 ( 0.5f , 0.5f ) ;
foreach ( var r in GachaScene . All ( "lobe" + Tier ) )
{
float a = r . amp * Mathf . Deg2Rad ;
var li = UiKit . Rect ( "Lobe" + lobes . Count , lobeRoot , mid , mid , new Vector2 ( Mathf . Cos ( a ) , Mathf . Sin ( a ) ) * r . x , new Vector2 ( r . w , r . h ) ) . gameObject . AddComponent < Image > ( ) ;
li . sprite = lobeSp ;
li . raycastTarget = false ;
var lc = r . color ;
lc . a = 0f ;
li . color = lc ;
if ( AddMat ! = null ) li . material = AddMat ;
li . enabled = lobeSp ! = null ;
li . rectTransform . localEulerAngles = new Vector3 ( 0f , 0f , r . amp ) ;
lobes . Add ( new Ray { rt = li . rectTransform , img = li , row = r } ) ;
}
bloom = Glow ( "Bloom" , lightRoot , lobeSp , cx , cy , dw * 2.7f , dh * 2.9f , light ) ; // 열린 문(밝은 사각) 둘레로 번지는 빛 = 사각의 딱딱한 가장자리를 누그러뜨림
coreImg = Glow ( "Core" , lightRoot , coreSp , cx , cy , tierRow . w , tierRow . w , light ) ;
coreRt = coreImg . rectTransform ;
leakImg = Glow ( "Leak" , lightRoot , streakSp , cx , cy , dh * 1.4f , doorRow . phase , edgeLight ) ;
leakRt = leakImg . rectTransform ;
leakRt . localEulerAngles = new Vector3 ( 0f , 0f , 90f ) ;
foreach ( var r in GachaScene . All ( "streak" + Tier ) )
{
var si = Glow ( "Streak" + streaks . Count , lightRoot , streakSp , cx , cy , r . w , r . h , r . color ) ;
si . rectTransform . localEulerAngles = new Vector3 ( 0f , 0f , r . amp ) ;
streaks . Add ( new Ray { rt = si . rectTransform , img = si , row = r } ) ;
}
BuildSilhouette ( ) ; // 빛 위 · 별가루 아래
bitLayer = UiKit . Fill ( "Bits" , fxLayer ) ;
BuildActors ( ) ;
}
/// <summary>
/// 활짝 열린 문 안에 선 영웅의 실루엣(PD 2026-11-13 「문이 활짝 열렸을 때 등장 영웅의 검은 실루엣만 보이게」 → 「빛에 가려져서 좀 더 옅은 형체만」) — 빛 속에 어렴풋한 형체만 · 제 색·이름은 카드에서.
/// 표 hero 줄: f_x,f_y 발 · f_h 표준 키 · f_max 말 탄 영웅 머리 끝 상한 · f_first 활짝 열리기 시작한 뒤 나타나기까지 · f_period 나타나는 초 · f_scale 처음 배율 · s_color 실루엣 색(알파 = 옅기) · f_amp,f_waves 실루엣 위에 덮이는 빛의 세기·크기(문 크기의 배).
/// </summary>
void BuildSilhouette ( )
{
heroRow = GachaScene . First ( "hero" ) ;
var h = Featured ! = null ? s . Def ( Featured . heroId ) : null ;
if ( h = = null | | heroRow = = null ) return ;
sil = UiKit . UnitStand ( "Silhouette" , fxLayer , h , new Vector2 ( 0f , 1f ) , new Vector2 ( heroRow . x , - heroRow . y ) , heroRow . h , heroRow . max > 1.001f ? heroRow . max : mountedTop ) ; // 말 탄 영웅도 문 안에 들어가게(표 f_max)
if ( sil = = null ) return ;
silImg = sil . GetComponent < Image > ( ) ;
var c = heroRow . color ;
c . a = 0f ;
silImg . color = c ;
UiFrameAnim . Attach ( silImg , UnitArt . Load ( h . art ) , "front_idle" ) ; // 실루엣도 숨은 쉼
// 실루엣 위에 덮이는 빛(PD 2026-11-13 「실루엣은 빛에 가려져서 좀 더 옅은 형체만」) — 가운데가 빛에 묻히고 가장자리만 어렴풋이
float dw = doorRow . w , dh = doorRow . h ;
var lc = tierRow . n = = 1 ? Color . white : Color . Lerp ( tierRow . color , Color . white , 0.55f ) ;
veil = Glow ( "Veil" , fxLayer , lobeSp , doorRow . x + dw * 0.5f , doorRow . y + dh * 0.5f , dw * heroRow . waves , dh * heroRow . waves , lc ) ; // 가운데가 뜨겁지 않은 고른 번짐(광원 그림은 가운데에 밝은 점이 맺힘)
}
/// <summary>발밑 그림자(표 shadow 줄 · 로비와 같은 부드러운 원 · f_scale 키의 사람 기준 크기에 배율 k).</summary>
RectTransform Shadow ( string name , float x , float y , float k )
{
var sp = shadowRow ! = null ? UISkin . Icon ( shadowRow . sprite ) : null ;
if ( sp = = null ) return null ;
var img = Spr ( name , shadowLayer , sp , x + shadowRow . x * k , y + shadowRow . y * k , shadowRow . w * k , shadowRow . h * k ) ;
img . color = shadowRow . color ;
return img . rectTransform ;
}
void BuildActors ( )
{
var bangRow = GachaScene . First ( "bang" ) ;
var bangSp = bangRow ! = null ? UISkin . Icon ( bangRow . sprite ) : null ;
foreach ( var r in GachaScene . All ( "actor" ) )
{
var data = UnitArt . Load ( r . sprite ) ;
Sprite [ ] idle ;
if ( data = = null | | ! data . Clips . TryGetValue ( IdleClip , out idle ) | | idle . Length = = 0 ) continue ;
float body = data . BodyPx > 0.5f ? data . BodyPx : idle [ 0 ] . rect . height ;
float k = r . h / body ;
var a = new Actor { data = data , idle = idle , phase = r . phase , flip = r . n = = 1 ? - 1f : 1f } ;
float pivotY = data . Meta . pivot ! = null & & data . Meta . pivot . Length > 1 ? data . Meta . pivot [ 1 ] : 0f ;
a . shadow = Shadow ( "Shadow" + r . slot , r . x , r . y , shadowRow ! = null & & shadowRow . scale > 0.5f ? r . h / shadowRow . scale : 1f ) ;
if ( a . shadow ! = null ) a . shadowHome = a . shadow . anchoredPosition ;
a . rt = Px ( "Actor" + r . slot , frontLayer , r . x , r . y , idle [ 0 ] . rect . width * k , idle [ 0 ] . rect . height * k , 0.5f , pivotY ) ;
a . home = a . rt . anchoredPosition ;
a . img = a . rt . gameObject . AddComponent < Image > ( ) ;
a . img . sprite = idle [ 0 ] ;
a . img . raycastTarget = false ;
a . rt . localScale = new Vector3 ( a . flip , 1f , 1f ) ;
a . idleOffset = Random . value * data . CycleSeconds ( IdleClip , idle . Length ) ;
Sprite [ ] re ;
if ( reactRow ! = null & & data . Clips . TryGetValue ( reactRow . sprite , out re ) & & re . Length > 0 ) a . react = re ;
if ( bangSp ! = null & & Reaction = = "shock" )
{
// 느낌표 = 머리 위(사람과 같이 뛰지 않게 장면 층에 따로)
a . bang = Spr ( "Bang" + r . slot , frontLayer , bangSp , r . x + bangRow . x * a . flip , r . y - r . h - bangRow . y , bangRow . w , bangRow . h , 0.5f , 0f ) . rectTransform ;
a . bang . localScale = Vector3 . zero ;
}
actors . Add ( a ) ;
}
foreach ( var a in actors )
if ( a . bang ! = null )
a . bang . SetAsLastSibling ( ) ;
}
// ---------- 시간 ----------
void Update ( )
{
if ( Manual ) return ;
Tick ( FixedDt > 0f ? FixedDt : Mathf . Min ( Time . unscaledDeltaTime , 0.1f ) ) ;
}
/// <summary>dt 초 진행(테스트·캡처는 직접 호출).</summary>
public void Tick ( float dt )
{
if ( stage = = null ) return ; // Play 중 재컴파일 = 직렬화 안 된 값이 비면 멈춤
t + = dt ;
Apply ( dt ) ;
FitStage ( ) ;
UpdateBits ( dt ) ;
if ( tCards > = 0f ) UpdateCards ( dt ) ;
else if ( t > = tEnd ) StartCards ( ) ; // 장면이 끝나면 저절로 카드로
}
static float EaseOut ( float u )
{
u = Mathf . Clamp01 ( u ) ;
return 1f - ( 1f - u ) * ( 1f - u ) ;
}
static float Smooth ( float u )
{
u = Mathf . Clamp01 ( u ) ;
return u * u * ( 3f - 2f * u ) ;
}
static float Back ( float u )
{
u = Mathf . Clamp01 ( u ) ;
const float c = 1.70158f ;
float v = u - 1f ;
return 1f + ( c + 1f ) * v * v * v + c * v * v ;
}
/// <summary>
/// 문 열림 곡선(0 닫힘 ~ 1 다 열림) — 앞부분(표 timeN f_waves 비율)은 틈이 천천히 벌어져 f_billow 까지(빛이 새는 구간 = 등급을 알게 됨) → 나머지는 활짝(끝에서 살짝 넘쳤다 돌아옴).
/// </summary>
float OpenAt ( float time )
{
if ( time < = tOpen ) return 0f ;
float u = ( time - tOpen ) / Mathf . Max ( 0.01f , timeRow . hit ) ;
float c = Mathf . Clamp ( timeRow . waves , 0.01f , 0.99f ) , b = timeRow . billow ;
if ( u < c ) return b * Mathf . Pow ( u / c , 1.5f ) ;
return b + ( 1f - b ) * Back ( ( u - c ) / ( 1f - c ) ) ;
}
void FitStage ( )
{
var pr = clip . rect ;
float k = Mathf . Max ( pr . width / Mathf . Max ( 1f , artW ) , pr . height / Mathf . Max ( 1f , artH ) ) ; // 화면을 덮게(비율이 달라도 빈 곳 없음)
// 다가가는 화면 = 덜컹거릴 때부터 문이 활짝 열릴 때까지 천천히 다가감 + 터지는 순간 살짝 더(표 zoom 줄)
float z = 1f ;
Vector2 focus = Vector2 . zero ;
if ( zoomRow ! = null )
{
float u = Smooth ( ( t - tRattle ) / Mathf . Max ( 0.01f , tBurst - tRattle ) ) ;
z = Mathf . Lerp ( zoomRow . min , zoomRow . max , u ) ;
if ( t > = tBurst ) z + = zoomRow . scale * Mathf . Exp ( - ( t - tBurst ) / Mathf . Max ( 0.01f , zoomRow . period ) ) ;
focus = new Vector2 ( zoomRow . x - artW * 0.5f , artH * 0.5f - zoomRow . y ) ;
}
float sc = k * z ;
if ( ! Mathf . Approximately ( sc , lastScale ) )
{
lastScale = sc ;
stage . localScale = new Vector3 ( sc , sc , 1f ) ;
}
// 흔들림 = 빛이 새는 동안 잔떨림(등급 줄 f_phase · 틈이 벌어질수록) + 활짝 열리는 순간(f_amp · 금방 잦아듦)
float amp = 0f ;
if ( tCards < 0f )
{
if ( t > = tBurst ) amp = tierRow . amp * Mathf . Clamp01 ( 1f - ( t - tBurst ) / Mathf . Max ( 0.01f , flashRow ! = null ? flashRow . hold : 0.4f ) ) ;
else if ( t > tOpen ) amp = tierRow . phase * Mathf . Clamp01 ( ( t - tOpen ) / Mathf . Max ( 0.01f , tBurst - tOpen ) ) ;
}
var off = focus * ( k * ( 1f - z ) ) ;
if ( amp > 0.05f ) off + = new Vector2 ( Mathf . Sin ( t * 71f ) * amp , Mathf . Cos ( t * 53f ) * amp ) * k ;
if ( stage . anchoredPosition ! = off ) stage . anchoredPosition = off ;
}
/// <summary>지금 시각 t 의 장면(문·빛·삼형제) — 건너뛰어도 같은 모습이 되게 시각의 함수로(입자만 그때그때 나옴).</summary>
void Apply ( float dt )
{
Stage = tCards > = 0f ? ( doneUi ? Step . Done : Step . Cards ) : t < tRattle ? Step . Wait : t < tOpen ? Step . Rattle : t < tBurst ? Step . Leak : Step . Open ;
SetA ( fade , Mathf . Clamp01 ( 1f - t / Mathf . Max ( 0.01f , timeRow . first ) ) ) ;
// 덜컹 = 박자마다 문짝이 떨림(뒤로 갈수록 세게) · 사이에는 멈춤(뜸)
Vector2 shake = Vector2 . zero ;
if ( t > = tRattle & & t < tOpen )
{
int n = Mathf . Max ( 1 , timeRow . count > 0 ? timeRow . count : rattleRow . count ) ;
float beat = ( tOpen - tRattle ) / n ;
int i = Mathf . Min ( n - 1 , Mathf . FloorToInt ( ( t - tRattle ) / beat ) ) ;
float u = ( t - tRattle - i * beat ) / beat ;
if ( i ! = lastBeat )
{
lastBeat = i ;
DoorDust ( rattleRow . n ) ;
}
if ( u < rattleRow . min )
{
float power = Mathf . Lerp ( 1f , rattleRow . max , n > 1 ? i / ( float ) ( n - 1 ) : 1f ) * ( 1f - u / rattleRow . min ) ;
float w = t * Mathf . PI * 2f / Mathf . Max ( 0.01f , rattleRow . period ) ;
shake = new Vector2 ( Mathf . Sin ( w ) , Mathf . Cos ( w * 1.3f ) * 0.35f ) * ( rattleRow . amp * power ) ;
}
}
leafL . anchoredPosition = leafLHome + shake ;
leafR . anchoredPosition = leafRHome + shake ;
float raw = OpenAt ( t ) ;
float o = Mathf . Clamp01 ( raw ) ;
DoorOpen = o ;
float leaf = Mathf . LerpUnclamped ( 1f , doorRow . min , raw ) ;
leafL . localScale = new Vector3 ( leaf , 1f , 1f ) ;
leafR . localScale = new Vector3 ( leaf , 1f , 1f ) ;
var leafC = Color . Lerp ( Color . white , doorRow . shade > = 0f ? new Color ( doorRow . shade , doorRow . shade , doorRow . shade , 1f ) : Color . white , o ) ;
if ( leafLImg . color ! = leafC ) leafLImg . color = leafRImg . color = leafC ;
ApplyLight ( dt , raw , o , leaf ) ;
if ( ! burstDone & & t > = tBurst )
{
burstDone = true ;
Burst ( ) ;
}
if ( flashT < 90f )
{
flashT + = dt ;
SetA ( flash , flashA * Mathf . Clamp01 ( 1f - flashT / Mathf . Max ( 0.01f , flashRow ! = null ? flashRow . period : 0.3f ) ) ) ;
}
if ( sil ! = null )
{
// 실루엣 = 문이 활짝 열리는 동안 빛 속에서 드러남(조금 작았다가 제 크기로)
float e = EaseOut ( ( t - tBurst - heroRow . first ) / Mathf . Max ( 0.01f , heroRow . period ) ) ;
float k = Mathf . Lerp ( heroRow . scale , 1f , e ) ;
sil . localScale = new Vector3 ( k , k , 1f ) ;
SetA ( silImg , e * heroRow . color . a ) ;
SetA ( veil , e * heroRow . amp * tierRow . scale ) ;
}
for ( int i = 0 ; i < actors . Count ; i + + ) ApplyActor ( actors [ i ] ) ;
}
/// <summary>
/// 빛 — 새는 동안: 틈의 빛(문 안 색) + 틈 번짐 + 세로 빛줄기 + 마당 빛(틈 폭만큼) · 두근거리듯 점점 빠르게 · 둘레는 덜컹거릴 때부터 서서히 어두워짐.
/// 활짝: 가운데 흰 광원이 커지고 길고 가는 빛줄기가 뻗으며 색 번짐이 퍼짐(천천히 돌고 숨쉼) · 별가루가 둘레에 계속 떠다님. 등급 = 표 tierN · streakN · lobeN · beamN 줄.
/// </summary>
void ApplyLight ( float dt , float raw , float o , float leaf )
{
float breath = 1f + Mathf . Sin ( t * Mathf . PI * 2f / Mathf . Max ( 0.1f , tierRow . period ) ) * tierRow . shade ;
float early = Mathf . Clamp01 ( o / 0.03f ) ;
float leakU = Mathf . Clamp01 ( ( t - tOpen ) / Mathf . Max ( 0.01f , tBurst - tOpen ) ) ;
float pulse = t < tBurst ? 1f + 0.2f * Mathf . Sin ( t * Mathf . PI * 2f * Mathf . Lerp ( 1.4f , 4f , leakU ) ) : 1f ;
LightLevel = Mathf . Sqrt ( o ) * early ;
if ( tierRow . n = = 1 )
{
// 무지개 등급 = 둘레 번짐 색이 천천히 바뀜(가운데는 늘 흰 빛)
var hue = Color . HSVToRGB ( Mathf . Repeat ( t * 0.45f , 1f ) , 0.6f , 1f ) ;
SetRgb ( halo , hue ) ;
}
SetA ( glow , early * tierRow . min ) ;
SetA ( halo , Mathf . Clamp01 ( 0.3f + 0.7f * Mathf . Sqrt ( o ) ) * early * tierRow . scale ) ;
float hs = Mathf . Lerp ( 0.45f , 1f , o ) * breath * pulse ;
haloRt . localScale = new Vector3 ( hs , hs , 1f ) ;
float gapW = doorRow . w * ( 1f - Mathf . Clamp ( leaf , 0f , 1f ) ) ;
slit . rectTransform . sizeDelta = new Vector2 ( gapW + doorRow . gapMin , doorRow . h * doorRow . gapMax ) ;
SetA ( slit , early * Mathf . Clamp01 ( 1.1f - o ) * Mathf . Clamp01 ( 0.8f * pulse ) * tierRow . scale ) ;
// 새는 동안의 세로 빛줄기(틈을 따라 위아래로 뻗음 · 활짝 열리면 사라짐)
leakRt . sizeDelta = new Vector2 ( doorRow . h * Mathf . Lerp ( 1.5f , 3.4f , leakU ) , doorRow . phase ) ;
SetA ( leakImg , early * Mathf . Clamp01 ( 1f - ( o - timeRow . billow ) / 0.35f ) * Mathf . Clamp01 ( pulse ) * tierRow . scale ) ;
// 마당 빛
for ( int i = 0 ; i < beams . Count ; i + + )
{
var b = beams [ i ] ;
SetA ( b . img , Mathf . Clamp01 ( 0.4f + o * 4f ) * early * b . row . scale * tierRow . scale ) ;
b . rt . localScale = new Vector3 ( Mathf . LerpUnclamped ( 0.06f , 1f , raw ) , Mathf . Clamp01 ( 0.45f + o * 2.5f ) * early , 1f ) ;
b . rt . localEulerAngles = new Vector3 ( 0f , 0f , b . row . amp * Mathf . Clamp01 ( o * 1.1f ) ) ; // 새는 동안은 색이 겹쳐 한 줄기 · 열릴수록 벌어짐
}
SetA ( bloom , Mathf . Clamp01 ( o * 3f ) * tierRow . scale * tierRow . hold ) ; // f_hold = 문 둘레 번짐 세기(★ 는 약하게 = 어두운 문 안에 둥근 빛 · 높은 등급은 문 전체가 빛에 잠김)
// 가운데 흰 광원
float cs = Mathf . Lerp ( 0.22f , 1f , EaseOut ( o ) ) * breath * pulse ;
coreRt . localScale = new Vector3 ( cs , cs , 1f ) ;
SetA ( coreImg , early * tierRow . scale ) ;
// 길고 가는 빛줄기 = 활짝 열릴 때 차례로 뻗고(끝에서 살짝 넘침) 천천히 흔들리며 깜빡
float bt = t - tBurst ;
for ( int i = 0 ; i < streaks . Count ; i + + )
{
var st = streaks [ i ] ;
float sb = bt > = st . row . phase ? Back ( ( bt - st . row . phase ) / 0.38f ) : 0f ;
st . rt . localScale = new Vector3 ( Mathf . Max ( 0f , sb ) * breath , 1f , 1f ) ;
st . rt . localEulerAngles = new Vector3 ( 0f , 0f , st . row . amp + st . row . waves * Mathf . Sin ( t * Mathf . PI * 2f / Mathf . Max ( 0.1f , st . row . period ) + i * 1.7f ) ) ;
SetA ( st . img , Mathf . Clamp01 ( sb ) * st . row . scale * ( 0.8f + 0.2f * Mathf . Sin ( t * st . row . speed + i * 2.1f ) ) ) ;
}
// 색 번짐 = 활짝 열릴 때 퍼지고 묶음째 천천히 돎
float lb = bt > = 0f ? EaseOut ( bt / 0.6f ) : 0f ;
float ls = lb * breath ;
lobeRoot . localScale = new Vector3 ( ls , ls , 1f ) ;
lobeRoot . localEulerAngles = new Vector3 ( 0f , 0f , t * tierRow . gapMax ) ;
for ( int i = 0 ; i < lobes . Count ; i + + ) SetA ( lobes [ i ] . img , lb * lobes [ i ] . row . scale * ( 0.82f + 0.18f * Mathf . Sin ( t * 1.3f + i * 1.9f ) ) ) ;
// 둘레 어둠
float dark = t < tBurst ? Smooth ( ( t - tRattle ) / Mathf . Max ( 0.01f , tBurst - tRattle ) ) : Mathf . Lerp ( 1f , tierRow . rest , Mathf . Clamp01 ( bt / 0.5f ) ) ;
SetA ( dimBack , dark * tierRow . max ) ;
// 별가루 = 새는 동안 틈에서 조금씩(표 f_gap_min 초당) · 열린 뒤 둘레에 계속(f_speed 초당 · 반경 f_billow)
if ( tCards < 0f & & dt > 0f & & o > 0.01f )
{
bool open = t > = tBurst ;
ambAcc + = dt * ( open ? tierRow . speed : tierRow . gapMin ) ;
while ( ambAcc > = 1f )
{
ambAcc - = 1f ;
Dust ( open , gapW ) ;
}
}
}
void ApplyActor ( Actor a )
{
float rt = t - tReact - a . phase ;
if ( rt < 0f | | a . react = = null | | reactRow = = null )
{
// 기다림 = 전용 그림 대기 숨 고르기(R22 · 유닛과 같은 규칙) + 첫 덜컹에 움찔
var sp = a . idle [ a . data . FrameAt ( IdleClip , a . idle . Length , t + a . idleOffset ) % a . idle . Length ] ;
if ( a . img . sprite ! = sp ) a . img . sprite = sp ;
float f = ( t - tRattle - a . phase ) / 0.28f ;
float flinch = f > 0f & & f < 1f ? rattleRow . hold * 4f * f * ( 1f - f ) : 0f ;
a . rt . anchoredPosition = a . home + new Vector2 ( 0f , flinch ) ;
return ;
}
float per = Mathf . Max ( 0.05f , reactRow . period ) ;
int hop = Mathf . FloorToInt ( rt / per ) ;
float u = rt / per - hop ;
bool hopping = hop < reactRow . count ;
float up = 0f , sx = 1f , sy = 1f , dx = 0f ;
if ( hopping )
{
float arc = 4f * u * ( 1f - u ) ;
up = reactRow . amp * arc * ( hop = = 0 ? 1f : reactRow . billow ) ;
// 뜰 때 길쭉 · 닿을 때 납작(짧게)
float land = Mathf . Clamp01 ( ( u - 0.82f ) / 0.18f ) + ( hop > 0 ? Mathf . Clamp01 ( 1f - u / 0.18f ) : 0f ) ;
sy = Mathf . Lerp ( Mathf . Lerp ( 1f , reactRow . max , arc ) , reactRow . min , Mathf . Clamp01 ( land ) ) ;
sx = 1f / Mathf . Sqrt ( Mathf . Max ( 0.3f , sy ) ) ;
}
else
{
// 뛴 뒤 = 반김: 작게 계속 들썩 · 놀람: 얼어붙어 덜덜
float after = rt - reactRow . count * per ;
if ( reactRow . waves > 0.01f ) dx = Mathf . Sin ( after * Mathf . PI * 2f / Mathf . Max ( 0.02f , reactRow . speed ) ) * reactRow . waves * Mathf . Lerp ( 1f , 0.35f , Mathf . Clamp01 ( after / 1.2f ) ) ;
else up = Mathf . Abs ( Mathf . Sin ( after * Mathf . PI / per ) ) * reactRow . amp * reactRow . shade ;
}
var frame = a . react [ Mathf . Min ( a . react . Length - 1 , hop % a . react . Length ) ] ;
if ( a . img . sprite ! = frame ) a . img . sprite = frame ;
float back = reactRow . first * Mathf . Clamp01 ( rt / per ) ; // 뒤로(화면 아래로) 물러남
a . rt . anchoredPosition = a . home + new Vector2 ( dx , up - back ) ;
a . rt . localScale = new Vector3 ( a . flip * sx , sy , 1f ) ;
if ( a . shadow ! = null )
{
float air = 1f - 0.3f * Mathf . Clamp01 ( up / Mathf . Max ( 1f , reactRow . amp ) ) ;
a . shadow . localScale = new Vector3 ( air , air , 1f ) ;
a . shadow . anchoredPosition = a . shadowHome + new Vector2 ( dx , - back ) ;
}
if ( a . bang ! = null )
{
float s1 = Back ( rt / 0.16f ) * ( 1f + 0.06f * Mathf . Sin ( rt * 14f ) ) ;
a . bang . localScale = new Vector3 ( s1 , s1 , 1f ) ;
}
}
// ---------- 입자(별가루 · 고리 · 먼지) ----------
Color Tint ( )
{
return tints [ Random . Range ( 0 , tints . Count ) ] ;
}
Sprite Sparkle ( )
{
return Random . value < 0.6f | | dotSp = = null ? starSp : dotSp ;
}
/// <summary>입자 하나 — glow = 덧셈 합성(빛) · drag = 속도가 줄어드는 정도 · tw = 깜빡임(초당 횟수 · 0 = 없음).</summary>
Bit Emit ( Transform parent , Sprite sp , Vector2 pos , Vector2 vel , float grav , float life , float s0 , float s1 , Color col , float spin , bool ring = false , bool glow = false , float drag = 0f , float tw = 0f )
{
if ( sp = = null ) return null ;
Bit b = null ;
foreach ( var x in bits )
if ( ! x . live )
{
b = x ;
break ;
}
if ( b = = null )
{
var go = new GameObject ( "Bit" , typeof ( RectTransform ) ) ;
b = new Bit { rt = ( RectTransform ) go . transform , img = go . AddComponent < Image > ( ) } ;
b . img . raycastTarget = false ;
bits . Add ( b ) ;
}
b . rt . SetParent ( parent , false ) ;
b . rt . anchorMin = b . rt . anchorMax = parent = = cardFx ? new Vector2 ( 0.5f , 0.5f ) : new Vector2 ( 0f , 1f ) ;
b . rt . pivot = new Vector2 ( 0.5f , 0.5f ) ;
b . rt . SetAsLastSibling ( ) ;
b . img . sprite = sp ;
b . img . material = glow ? AddMat : null ;
b . img . color = col ;
b . pos = pos ;
b . vel = vel ;
b . grav = grav ;
b . drag = drag ;
b . t = 0f ;
b . life = Mathf . Max ( 0.05f , life ) ;
b . s0 = s0 ;
b . s1 = s1 ;
b . a0 = col . a ;
b . spin = spin ;
b . tw = tw ;
b . ph = Random . value * 6.283f ;
b . ring = ring ;
b . live = true ;
b . rt . localEulerAngles = Vector3 . zero ;
b . rt . gameObject . SetActive ( true ) ;
StepBit ( b , 0f ) ;
return b ;
}
void StepBit ( Bit b , float dt )
{
b . t + = dt ;
float u = b . t / b . life ;
if ( u > = 1f )
{
b . live = false ;
b . rt . gameObject . SetActive ( false ) ;
return ;
}
if ( b . drag > 0f ) b . vel * = Mathf . Exp ( - b . drag * dt ) ;
b . vel . y - = b . grav * dt ;
b . pos + = b . vel * dt ;
b . rt . anchoredPosition = b . pos ;
float size = Mathf . Lerp ( b . s0 , b . s1 , b . ring ? EaseOut ( u ) : u ) ;
b . rt . sizeDelta = new Vector2 ( size , size ) ;
if ( Mathf . Abs ( b . spin ) > 0.01f ) b . rt . localEulerAngles = new Vector3 ( 0f , 0f , b . spin * b . t ) ;
float env = b . ring ? 1f - u : u < 0.1f ? u / 0.1f : u > 0.55f ? 1f - ( u - 0.55f ) / 0.45f : 1f ;
if ( b . tw > 0f ) env * = 0.55f + 0.45f * Mathf . Sin ( b . t * b . tw * 6.283f + b . ph ) ;
SetA ( b . img , b . a0 * env ) ;
}
void UpdateBits ( float dt )
{
for ( int i = 0 ; i < bits . Count ; i + + )
if ( bits [ i ] . live )
StepBit ( bits [ i ] , dt ) ;
}
Vector2 DoorCenter
{
get { return new Vector2 ( doorRow . x + doorRow . w * 0.5f , - ( doorRow . y + doorRow . h * 0.5f ) ) ; }
}
/// <summary>떠다니는 별가루 한 알 — 새는 동안 = 틈에서 흘러나옴 · 열린 뒤 = 문 둘레(표 등급 줄 f_billow 반경)에 천천히 떠다니며 깜빡.</summary>
void Dust ( bool open , float gapW )
{
if ( sparkRow = = null ) return ;
Vector2 from , vel ;
if ( open )
{
float a = Random . Range ( 0f , Mathf . PI * 2f ) , d = Mathf . Sqrt ( Random . value ) * tierRow . billow ;
var dir = new Vector2 ( Mathf . Cos ( a ) , Mathf . Sin ( a ) ) ;
from = DoorCenter + dir * d ;
vel = dir * Random . Range ( 8f , 46f ) + new Vector2 ( 0f , Random . Range ( 4f , 30f ) ) ;
}
else
{
float side = Random . value < 0.5f ? - 1f : 1f ;
from = DoorCenter + new Vector2 ( side * gapW * 0.5f * Random . value , Random . Range ( - 0.45f , 0.45f ) * doorRow . h ) ;
vel = new Vector2 ( side * Random . Range ( 10f , 70f ) , Random . Range ( - 60f , 40f ) ) ;
}
Emit ( bitLayer , Sparkle ( ) , from , vel , sparkRow . amp * 0.3f , sparkRow . period * Random . Range ( 0.6f , 1.2f ) , Random . Range ( sparkRow . w , sparkRow . h ) , sparkRow . w * 0.5f , Tint ( ) , 0f , false , true , 0.6f , sparkRow . billow ) ;
}
/// <summary>덜컹거릴 때 문 위 모서리에서 떨어지는 먼지(등급과 무관한 색 — 아직 등급을 알리지 않음).</summary>
void DoorDust ( int n )
{
if ( puffRow = = null ) return ;
for ( int i = 0 ; i < n ; i + + )
{
float side = i % 2 = = 0 ? - 1f : 1f ;
var from = new Vector2 ( doorRow . x + doorRow . w * ( 0.5f + side * Random . Range ( 0.3f , 0.5f ) ) , - ( doorRow . y + Random . Range ( 0f , doorRow . h * 0.15f ) ) ) ;
Emit ( bitLayer , puffSp , from , new Vector2 ( side * rattleRow . speed * Random . Range ( 0.3f , 1f ) , - rattleRow . speed * Random . Range ( 0.2f , 0.6f ) ) , rattleRow . speed * 1.5f ,
puffRow . period * Random . Range ( 0.9f , 1.3f ) , rattleRow . w , rattleRow . h , puffRow . color , 0f ) ;
}
}
/// <summary>문이 활짝 열리는 순간 — 번쩍 + 흔들림(FitStage) + 얇게 퍼지는 고리 + 사방으로 튀어 나가 느려지며 깜빡이는 별가루(수 = 등급 줄).</summary>
void Burst ( )
{
flashA = tierRow . first ;
flashT = 0f ;
if ( sparkRow ! = null )
for ( int i = 0 ; i < tierRow . count ; i + + )
{
float ang = Random . Range ( 0f , Mathf . PI * 2f ) ;
float sp = sparkRow . speed * Random . Range ( 0.15f , 1f ) ;
var from = DoorCenter + new Vector2 ( Random . Range ( - 0.4f , 0.4f ) * doorRow . w , Random . Range ( - 0.4f , 0.4f ) * doorRow . h ) ;
Emit ( bitLayer , Sparkle ( ) , from , new Vector2 ( Mathf . Cos ( ang ) , Mathf . Sin ( ang ) ) * sp , sparkRow . amp , sparkRow . period * Random . Range ( 0.7f , 1.5f ) ,
Random . Range ( sparkRow . w , sparkRow . h ) , sparkRow . w * 0.4f , Tint ( ) , Random . Range ( - 90f , 90f ) , false , true , sparkRow . waves , sparkRow . billow ) ;
}
if ( ringRow = = null ) return ;
int rings = Mathf . RoundToInt ( tierRow . hit ) ;
for ( int i = 0 ; i < rings ; i + + )
{
var col = Tint ( ) ;
col . a = ringRow . scale ;
var b = Emit ( bitLayer , ringSp , DoorCenter , Vector2 . zero , 0f , ringRow . period * ( 1f + i * ringRow . phase ) , ringRow . w , ringRow . h * ( 1f + i * 0.35f ) , col , 0f , true , true ) ;
if ( b ! = null ) b . rt . SetAsFirstSibling ( ) ;
}
}
// ---------- 글자(Event 층) ----------
GameObject Hint ( string text , int size )
{
var tx = UiKit . Title ( UiKit . Label ( ui , text , size , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 96f ) , new Vector2 ( 900f , 70f ) ) , true ) ;
tx . raycastTarget = false ;
tx . gameObject . AddComponent < UiPulse > ( ) ;
return tx . gameObject ;
}
// ---------- 탭 ----------
/// <summary>화면 탭(버튼) — 캡처·테스트가 직접 진행하는 동안(Manual)은 실제 마우스·터치를 받지 않는다(캡처 창을 누가 눌러도 장면이 넘어가지 않게).</summary>
void OnPointer ( )
{
if ( ! Manual ) Tap ( ) ;
}
/// <summary>장면 중 = 건너뛰어 카드로(PD 「터치로 문이 열리는 장면을 스킵」) · 카드가 떨어지는 중 = 전부 공개 · 다 나온 뒤 = 닫기.</summary>
public void Tap ( )
{
if ( tCards < 0f ) StartCards ( ) ;
else if ( CardsLanded < cards . Count ) LandAll ( ) ;
else Close ( ) ;
}
/// <summary>카드 획득 단계로(장면 = 문이 열린 모습으로 어두운 막 뒤에).</summary>
public void StartCards ( )
{
if ( tCards > = 0f ) return ;
if ( t < tFull )
{
// 건너뜀 = 문이 다 열린 모습으로(터지는 연출은 생략)
t = tFull ;
burstDone = true ;
Apply ( 0f ) ;
FitStage ( ) ;
}
tCards = t ;
if ( hint ! = null ) Destroy ( hint ) ;
shade . gameObject . SetActive ( true ) ;
SetA ( shade , 0f ) ;
BuildCards ( ) ;
Stage = Step . Cards ;
}
public void LandAll ( )
{
if ( tCards < 0f ) StartCards ( ) ;
foreach ( var c in cards )
if ( ! c . landed )
Land ( c , true ) ;
foreach ( var c in cards ) Settle ( c , 1f ) ;
FinishCards ( ) ;
}
public void Close ( )
{
var cb = onClose ;
onClose = null ;
if ( cb ! = null ) cb ( ) ;
else Destroy ( gameObject ) ;
}
// ---------- 카드 획득(PD 레퍼런스: 카드가 UI 에 등장할 때의 연출) ----------
void BuildCards ( )
{
var ribbon = UiKit . Rect ( "Title" , ui , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 170f ) , new Vector2 ( 620f , 130f ) ) ;
if ( UISkin . Has ( "ribbon_red" ) ) UiKit . Skin ( ribbon , "ribbon_red" , UiKit . White ) ;
2026-11-13 13:34:16 +00:00
UiKit . Title ( UiKit . Label ( ribbon , "등용 결과" , 58 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 12f ) , new Vector2 ( 520f , 90f ) ) , true ) . raycastTarget = false ;
2026-11-13 11:29:46 +00:00
// 카드마다 그 뽑기 직후의 조각 수(같은 영웅이 여러 번 나오면 차례로 쌓임 · 세이브에는 이미 다 더해져 있음)
var total = new Dictionary < string , int > ( ) ;
foreach ( var p in pulls )
{
int v ;
total . TryGetValue ( p . heroId , out v ) ;
total [ p . heroId ] = v + p . shards ;
}
var run = new Dictionary < string , int > ( ) ;
int cols = Mathf . Max ( 1 , Mathf . Min ( cardRow . n > 0 ? cardRow . n : 5 , pulls . Count ) ) ;
int rows = ( pulls . Count + cols - 1 ) / cols ;
float W = ( ( RectTransform ) transform ) . rect . width ;
float gapX = cardRow . gapMax , gapY = cardRow . billow , strip = cardRow . h ;
float cw = Mathf . Min ( cardRow . w , ( W - cardRow . x * 2f - ( cols - 1 ) * gapX ) / cols ) ;
float k = cw / CollectionCards . CellSize . x ;
float ch = CollectionCards . CellSize . y * k ;
float pitchY = ch + strip + gapY ;
float top = ( rows * pitchY - gapY ) * 0.5f + cardRow . y ;
var mid = new Vector2 ( 0.5f , 0.5f ) ;
for ( int i = 0 ; i < pulls . Count ; i + + )
{
var p = pulls [ i ] ;
var h = s . Def ( p . heroId ) ;
int r = i / cols , c = i % cols ;
int inRow = Mathf . Min ( cols , pulls . Count - r * cols ) ;
var home = new Vector2 ( ( c - ( inRow - 1 ) * 0.5f ) * ( cw + gapX ) , top - r * pitchY - ( ch + strip ) ) ;
var card = new Card { home = home , tier = p . tier , start = cardRow . first + i * cardRow . gapMin } ;
// 등급 꾸밈(카드 뒤 · 닿을 때 켜짐): ★★ = 금빛 번짐 + 주황 테두리 · ★★★ = 흰 빛 번짐 + 도는 가는 빛줄기 + 색이 바뀌는 테두리 + 별가루 — 다 나온 뒤에도 등급이 한눈에
var tr = GachaScene . Tier ( p . tier ) ;
if ( p . tier > = 2 & & tr ! = null )
{
card . rainbow = tr . n = = 1 ;
card . deco = UiKit . Rect ( "Deco" + i , cardsLayer , mid , mid , home + new Vector2 ( 0f , strip + ch * 0.5f ) , new Vector2 ( cw , ch ) * cardRow . max * ( card . rainbow ? 1.35f : 1f ) ) ;
card . glow = card . deco . gameObject . AddComponent < Image > ( ) ;
card . glow . sprite = coreSp ;
card . glow . raycastTarget = false ;
card . glow . color = card . rainbow ? Color . white : tr . color ;
if ( AddMat ! = null ) card . glow . material = AddMat ;
card . glow . enabled = coreSp ! = null ;
if ( card . rainbow & & streakSp ! = null )
{
card . rays = UiKit . Rect ( "Rays" , card . deco , mid , mid , Vector2 . zero , Vector2 . zero ) ;
for ( int q = 0 ; q < 2 ; q + + )
{
var ri = UiKit . Rect ( "Ray" + q , card . rays , mid , mid , Vector2 . zero , new Vector2 ( ch * cardRow . max * 1.7f , cardRow . ready ) ) . gameObject . AddComponent < Image > ( ) ;
ri . sprite = streakSp ;
ri . raycastTarget = false ;
if ( AddMat ! = null ) ri . material = AddMat ;
ri . rectTransform . localEulerAngles = new Vector3 ( 0f , 0f , 28f + q * 84f ) ;
}
}
card . edge = UiKit . Paint ( UiKit . Rect ( "Edge" , card . deco , mid , mid , Vector2 . zero , new Vector2 ( cw , ch ) + Vector2 . one * ( cardRow . stand + ( card . rainbow ? 8f : 0f ) ) ) , tr . color , true , false ) ;
card . deco . gameObject . SetActive ( false ) ;
}
card . rt = UiKit . Rect ( "Card" + i , cardsLayer , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0f ) , home , new Vector2 ( cw , ch + strip ) ) ;
var box = UiKit . Rect ( "Box" , card . rt , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , Vector2 . zero , new Vector2 ( cw , ch ) ) ;
if ( h ! = null ) CollectionCards . CardIn ( box , s , h , false , null , k ) ;
if ( p . isNew )
{
// NEW = 카드 위 가장자리 가운데에 걸침(이름·병과 심볼을 가리지 않게)
var nw = UiKit . Rect ( "New" , box , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0f , 12f ) , new Vector2 ( 86f , 36f ) ) ;
UiKit . Skin ( nw , "tail" , UiKit . Col ( "tail_red" , UiKit . Red ) ) . enabled = UISkin . Has ( "tail" ) ;
UiKit . Title ( UiKit . FillLabel ( nw , "NEW" , 24 , UiKit . White ) , true ) ;
}
if ( card . rainbow & & streakSp ! = null )
{
// ★★★ = 카드 위를 비스듬히 지나가는 빛(카드 밖은 잘림 · 닿은 뒤 되풀이)
var mask = UiKit . Fill ( "Sheen" , box ) ;
mask . gameObject . AddComponent < RectMask2D > ( ) ;
var sh = UiKit . Rect ( "Line" , mask , mid , mid , new Vector2 ( - cw * 2f , 0f ) , new Vector2 ( ch * 1.7f , cw * 0.4f ) ) . gameObject . AddComponent < Image > ( ) ;
sh . sprite = streakSp ;
sh . raycastTarget = false ;
if ( AddMat ! = null ) sh . material = AddMat ;
sh . color = new Color ( 1f , 1f , 1f , 0.7f ) ;
sh . rectTransform . localEulerAngles = new Vector3 ( 0f , 0f , 62f ) ;
card . sheen = sh . rectTransform ;
card . sheenW = cw ;
}
var st = UiKit . Rect ( "Strip" , card . rt , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , Vector2 . zero , new Vector2 ( cw , strip ) ) ;
BuildStrip ( card , st , p , h , cw , strip , total , run ) ;
card . strip = st ;
st . gameObject . SetActive ( false ) ; // 조각 정보는 카드가 닿은 뒤에(떨어지는 동안 다른 카드를 가리지 않게)
card . flash = UiKit . Paint ( UiKit . Fill ( "Flash" , box ) , Color . white , true , false ) ;
card . rt . gameObject . SetActive ( false ) ;
cards . Add ( card ) ;
}
cardFx . SetAsLastSibling ( ) ;
}
/// <summary>카드 아래 조각 정보 = 조각 아이콘 + 게이지(보유 / 다음 ★ 필요) + 「+N」(PD 2026-11-13 「영웅을 획득할 때 조각 정보」) · 병사 중복 = 골드.</summary>
void BuildStrip ( Card card , RectTransform st , GachaPull p , HeroDb . Hero h , float cw , float strip , Dictionary < string , int > total , Dictionary < string , int > run )
{
if ( h = = null ) return ;
if ( h . isSoldier )
{
if ( p . gold < = 0 ) return ;
UiKit . IconOrDot ( st , "cur_gold" , "골" , UiKit . Coin , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( cw * 0.16f , 0f ) , strip * 0.72f ) ;
UiKit . Title ( UiKit . Label ( st , "+" + p . gold , 30 , UiKit . White , TextAnchor . MiddleLeft , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( cw * 0.16f + strip * 0.8f , 0f ) , new Vector2 ( cw * 0.6f , strip ) ) , true ) ;
return ;
}
var hs = s . Save . Hero ( h . id ) ;
int have = hs ! = null ? hs . shards : 0 ;
int sum , done ;
total . TryGetValue ( h . id , out sum ) ;
run . TryGetValue ( h . id , out done ) ;
int after = have - sum + done + p . shards ;
run [ h . id ] = done + p . shards ;
int need = hs ! = null ? HeroRules . ShardsForNext ( s . Db , hs ) : 0 ;
float icon = strip * 0.74f ;
UiKit . IconOrDot ( st , "cur_shard" , "조" , UiKit . Purple , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 2f , 0f ) , icon ) ;
float bw = cw - icon - 10f ;
card . gauge = UiKit . Bar ( "ShardBar" , st , new Vector2 ( 0f , 0.5f ) , new Vector2 ( 0f , 0.5f ) , new Vector2 ( icon + 6f , 0f ) , new Vector2 ( bw , strip * 0.56f ) , UiKit . Slot , UiKit . Col ( "grade_purple" , UiKit . Purple ) ) ;
card . g1 = need > 0 ? Mathf . Clamp01 ( after / ( float ) need ) : 1f ;
card . g0 = need > 0 ? Mathf . Clamp01 ( ( after - p . shards ) / ( float ) need ) : 1f ;
UiKit . SetBar ( card . gauge , card . g0 ) ;
string num = need > 0 ? after + "/" + need : after + " · 최고" ;
if ( p . shards > 0 ) num + = " <color=#FFE066>+" + p . shards + "</color>" ;
var tx = UiKit . Title ( UiKit . Label ( card . gauge . parent , num , 24 , UiKit . White , TextAnchor . MiddleCenter , new Vector2 ( 0.5f , 0.5f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , new Vector2 ( bw , strip ) ) , true ) ;
tx . supportRichText = true ;
tx . horizontalOverflow = HorizontalWrapMode . Overflow ;
}
void UpdateCards ( float dt )
{
float ct = t - tCards ;
SetA ( shade , Mathf . Clamp01 ( ct / 0.2f ) * ( dimRow ! = null ? dimRow . color . a : 0.85f ) ) ;
bool all = true ;
foreach ( var c in cards )
{
float u = ct - c . start ;
if ( u < 0f )
{
all = false ;
continue ;
}
if ( ! c . rt . gameObject . activeSelf ) c . rt . gameObject . SetActive ( true ) ;
float drop = Mathf . Max ( 0.01f , cardRow . period ) ;
if ( u < drop )
{
// 떨어짐 = 위에서 빠르게 · 길쭉하게 늘어남
all = false ;
float f = u / drop ;
c . rt . anchoredPosition = c . home + new Vector2 ( 0f , cardRow . amp * ( 1f - f * f ) ) ;
float sy = Mathf . Lerp ( cardRow . speed , 1f , f * f * 0.5f ) ;
c . rt . localScale = new Vector3 ( 1f / Mathf . Sqrt ( sy ) , sy , 1f ) ;
SetA ( c . flash , 0f ) ;
continue ;
}
if ( ! c . landed ) Land ( c , false ) ;
Settle ( c , ( u - drop ) / Mathf . Max ( 0.01f , cardRow . hold ) ) ;
SetA ( c . flash , Mathf . Clamp01 ( 1f - ( u - drop ) / Mathf . Max ( 0.01f , cardRow . hit ) ) ) ;
if ( c . gauge ! = null ) UiKit . SetBar ( c . gauge , Mathf . Lerp ( c . g0 , c . g1 , EaseOut ( ( u - drop - 0.12f ) / 0.35f ) ) ) ;
if ( c . rays ! = null ) c . rays . localEulerAngles = new Vector3 ( 0f , 0f , t * 24f ) ;
if ( c . rainbow )
{
var hue = Color . HSVToRGB ( Mathf . Repeat ( t * 0.8f , 1f ) , 0.7f , 1f ) ;
SetRgb ( c . edge , hue ) ;
if ( c . sheen ! = null )
{
float f = Mathf . Repeat ( ( u - drop ) / 1.8f , 1f ) ;
c . sheen . anchoredPosition = new Vector2 ( Mathf . Lerp ( - c . sheenW * 1.3f , c . sheenW * 1.3f , Mathf . Clamp01 ( f / 0.42f ) ) , 0f ) ;
}
SetRgb ( c . glow , Color . Lerp ( hue , Color . white , 0.35f ) ) ;
// ★★★ 카드 둘레에 별가루가 계속
if ( twinkleRow ! = null & & dt > 0f )
{
c . amb + = dt * twinkleRow . speed ;
while ( c . amb > = 1f )
{
c . amb - = 1f ;
CardSparkle ( c , Tint ( ) ) ;
}
}
}
}
if ( all & & ! doneUi & & ct > = ( cards . Count > 0 ? cards [ cards . Count - 1 ] . start : 0f ) + cardRow . period + cardRow . rest ) FinishCards ( ) ;
}
void CardSparkle ( Card c , Color col )
{
var size = c . rt . sizeDelta ;
var from = c . home + new Vector2 ( Random . Range ( - 0.62f , 0.62f ) * size . x , Random . Range ( 0.08f , 1.06f ) * size . y ) ;
col . a = 1f ;
Emit ( cardFx , Sparkle ( ) , from , new Vector2 ( Random . Range ( - 1f , 1f ) , Random . Range ( 0.2f , 1f ) ) * twinkleRow . amp , 0f , twinkleRow . period * Random . Range ( 0.7f , 1.3f ) ,
Random . Range ( twinkleRow . w , twinkleRow . h ) , twinkleRow . w * 0.4f , col , Random . Range ( - 90f , 90f ) , false , true , 0.5f , twinkleRow . billow ) ;
}
/// <summary>닿은 뒤 = 납작하게 눌렸다가 튀어 오르며 제 모양으로(u 0 → 1).</summary>
void Settle ( Card c , float u )
{
u = Mathf . Clamp01 ( u ) ;
c . rt . anchoredPosition = c . home ;
float sq = 1f + ( cardRow . min - 1f ) * Mathf . Cos ( u * Mathf . PI * 2.5f ) * ( 1f - u ) * ( 1f - u ) ;
c . rt . localScale = new Vector3 ( 1f / Mathf . Sqrt ( Mathf . Max ( 0.3f , sq ) ) , sq , 1f ) ;
if ( u > = 1f )
{
SetA ( c . flash , 0f ) ;
if ( c . gauge ! = null ) UiKit . SetBar ( c . gauge , c . g1 ) ;
}
}
void Land ( Card c , bool quiet )
{
c . landed = true ;
if ( ! c . rt . gameObject . activeSelf ) c . rt . gameObject . SetActive ( true ) ;
SetA ( c . flash , quiet ? 0f : 1f ) ;
if ( c . deco ! = null ) c . deco . gameObject . SetActive ( true ) ;
if ( c . strip ! = null ) c . strip . gameObject . SetActive ( true ) ;
if ( quiet ) return ;
var size = c . rt . sizeDelta ;
// 먼지 구름 = 카드 발치 양옆으로 퍼짐 · 별가루 = 카드 둘레에서 깜빡(등급이 높을수록 많이)
if ( puffRow ! = null )
for ( int i = 0 ; i < puffRow . count ; i + + )
{
float side = i % 2 = = 0 ? - 1f : 1f ;
var from = c . home + new Vector2 ( side * size . x * Random . Range ( 0.25f , 0.5f ) , Random . Range ( 0f , size . y * 0.25f ) ) ;
Emit ( cardFx , puffSp , from , new Vector2 ( side * puffRow . amp * Random . Range ( 0.5f , 1f ) , puffRow . amp * Random . Range ( 0.1f , 0.6f ) ) , 0f ,
puffRow . period * Random . Range ( 0.8f , 1.2f ) , puffRow . w , puffRow . h , puffRow . color , 0f ) ;
}
if ( twinkleRow ! = null )
{
var tr = GachaScene . Tier ( c . tier ) ;
int n = twinkleRow . count * c . tier ;
for ( int i = 0 ; i < n ; i + + ) CardSparkle ( c , c . tier = = Tier ? Tint ( ) : tr ! = null ? tr . color : Color . white ) ;
}
}
void FinishCards ( )
{
if ( doneUi ) return ;
doneUi = true ;
Stage = Step . Done ;
SetA ( shade , dimRow ! = null ? dimRow . color . a : 0.85f ) ;
UiKit . Btn ( "Ok" , ui , "확인" , 40 , UiKit . Red , UiKit . White , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 180f ) , new Vector2 ( 420f , 110f ) , Close ) ;
hint = Hint ( "화면을 터치하면 닫힙니다" , 34 ) ;
}
}
}