ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UI/LobbyAmbient.cs

294 lines
12 KiB
C#
Raw 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.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 로비 배경 연출(PD 2026-11-11 「펄럭이는 깃발 · 환하게 비쳐진 관청의 불빛이나 등불 · 가끔 날아다니는 새」) — 값 = 표 lobby_scene.csv(LobbyScene · C45).
/// 한 Frame 캔버스(「Ambient」 · 배경 그림 위 · 영웅 아래)에 모두 담는다 — 매 프레임 바뀌는 것끼리만 다시 묶임(P41 · 화면 캔버스는 그대로).
/// 좌표 = 배경 그림 px(그림 크기 artW × artH) → 이 층(그림을 꽉 채움)의 단위로 매 프레임 환산(그림이 화면 폭에 맞춰 커져도 같은 자리).
/// </summary>
public class LobbyAmbient : MonoBehaviour
{
class Glow
{
public Graphic g;
public LobbyScene.Row row;
public float baseA;
}
class Mover
{
public RectTransform rt;
public Image img;
public Sprite[] frames;
public float x;
public float y;
public float vx;
public float vy;
public float size;
public float t;
public float life;
public float ph;
public float rot;
public float vrot;
public bool on;
}
RectTransform area;
RectTransform back;
RectTransform front;
float artW;
float artH;
readonly List<Glow> glows = new List<Glow>();
readonly List<Mover> birds = new List<Mover>();
readonly List<Mover> petals = new List<Mover>();
LobbyScene.Row birdRow;
LobbyScene.Row petalRow;
float nextFlock;
float nextPetal;
float openedAt;
/// <summary>영웅 자리 층(그림을 꽉 채움 · 그림 좌표 = 정규화 앵커) — 뒤 연출(깃발·불빛)과 앞 연출(새·꽃잎) 사이.</summary>
public RectTransform Lineup { get; private set; }
/// <summary>
/// 그림(art) 위에 장면 층(Frame 캔버스 1장)을 만든다 — 순서 = Back(깃발·불빛) → Lineup(그림자·영웅 · 대기 프레임 재생) → Front(새·꽃잎 · PD 2026-11-11 「벚꽃잎은 유닛보다 앞」).
/// 영웅 대기 프레임이 매 프레임 바뀌므로 영웅도 이 Frame 층에 둔다(화면 캔버스 = Event 그대로 · P41).
/// </summary>
public static LobbyAmbient Build(RectTransform art, float artW, float artH)
{
var layer = UiLayer.Group("Ambient", art, UiRate.Frame);
var a = layer.gameObject.AddComponent<LobbyAmbient>();
a.area = layer;
a.back = UiKit.Fill("Back", layer);
a.Lineup = UiKit.Fill("Lineup", layer);
a.front = UiKit.Fill("Front", layer);
a.artW = artW;
a.artH = artH;
a.Setup();
return a;
}
/// <summary>그림 px 사각형(왼쪽 위 x,y · 폭 w · 높이 h) = 정규화 앵커(그림과 함께 늘어남) · 뒤 층.</summary>
RectTransform ArtRect(string name, float x, float y, float w, float h)
{
var rt = UiKit.Rect(name, back, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
rt.anchorMin = new Vector2(x / artW, 1f - (y + h) / artH);
rt.anchorMax = new Vector2((x + w) / artW, 1f - y / artH);
rt.offsetMin = rt.offsetMax = Vector2.zero;
return rt;
}
static Sprite[] Load(LobbyScene.Row r)
{
var list = new List<Sprite>();
foreach (var id in r.Sprites)
{
var sp = UISkin.Icon(id);
if (sp != null) list.Add(sp);
}
return list.ToArray();
}
void Setup()
{
// 깃발(원래 깃발 위에 같은 자리 · UiWaveImage)
foreach (var r in LobbyScene.All("flag"))
{
var sp = Load(r);
if (sp.Length == 0) continue;
var rt = ArtRect("Flag_" + sp[0].name, r.x, r.y, r.w, r.h);
var wv = rt.gameObject.AddComponent<UiWaveImage>();
wv.sprite = sp[0];
wv.raycastTarget = false;
wv.amp = r.amp;
wv.period = r.period;
wv.waves = r.waves;
wv.phase = r.phase;
wv.billow = Mathf.Max(0f, r.billow);
if (r.shade >= 0f) wv.shade = r.shade;
wv.color = r.color;
}
// 불빛(관청 창 · 문 앞 번짐 · 석등) = 알파 깜박임
foreach (var r in LobbyScene.All("light"))
{
var sp = Load(r);
if (sp.Length == 0) continue;
var img = ArtRect("Light_" + sp[0].name, r.x, r.y, r.w, r.h).gameObject.AddComponent<Image>();
img.sprite = sp[0];
img.raycastTarget = false;
img.color = r.color;
glows.Add(new Glow { g = img, row = r, baseA = r.color.a });
}
birdRow = LobbyScene.First("bird");
petalRow = LobbyScene.First("petal");
openedAt = Time.unscaledTime;
if (birdRow != null)
{
var fr = Load(birdRow);
for (int i = 0; i < Mathf.Max(0, birdRow.count) && fr.Length > 0; i++) birds.Add(NewMover("Bird" + i, fr));
nextFlock = openedAt + birdRow.first;
}
if (petalRow != null)
{
var fr = Load(petalRow);
for (int i = 0; i < Mathf.Max(0, petalRow.count) && fr.Length > 0; i++) petals.Add(NewMover("Petal" + i, fr));
nextPetal = openedAt + petalRow.first;
}
}
Mover NewMover(string name, Sprite[] frames)
{
var rt = UiKit.Rect(name, front, new Vector2(0f, 1f), new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
var img = rt.gameObject.AddComponent<Image>();
img.raycastTarget = false;
img.sprite = frames[0];
rt.gameObject.SetActive(false);
return new Mover { rt = rt, img = img, frames = frames };
}
void OnEnable()
{
// 화면을 다시 열면 새 떼 · 꽃잎 시계를 다시 시작(열자마자 하나는 보이게 = 표 f_first)
float now = Time.unscaledTime;
if (birdRow != null) nextFlock = now + birdRow.first;
if (petalRow != null) nextPetal = now + petalRow.first;
foreach (var m in birds) Hide(m);
foreach (var m in petals) Hide(m);
}
static void Hide(Mover m)
{
m.on = false;
if (m.rt != null) m.rt.gameObject.SetActive(false);
}
static float Flicker(LobbyScene.Row r, float t)
{
// 촛불 = 느린 물결 + 빠른 떨림(두 사인의 합 · 0~1)
float p = Mathf.Max(0.05f, r.period);
float s = 0.62f * Mathf.Sin((t / p + r.phase) * Mathf.PI * 2f) + 0.38f * Mathf.Sin((t / p * 2.71f + r.phase * 1.7f) * Mathf.PI * 2f);
return Mathf.Lerp(r.min, r.max, 0.5f + 0.5f * s);
}
void Update()
{
float t = Time.unscaledTime;
float dt = Mathf.Min(0.1f, Time.unscaledDeltaTime);
float unit = area.rect.width / Mathf.Max(1f, artW);
if (unit <= 0f) return;
foreach (var g in glows)
{
var c = g.g.color;
c.a = g.baseA * Flicker(g.row, t);
g.g.color = c;
}
UpdateBirds(t, dt, unit);
UpdatePetals(t, dt, unit);
}
void UpdateBirds(float t, float dt, float unit)
{
if (birdRow == null || birds.Count == 0) return;
bool any = false;
foreach (var b in birds) any |= b.on;
if (!any && t >= nextFlock)
{
// 새 떼 1번 — 왼→오 또는 오→왼 · 앞장선 새 뒤로 비스듬히
int n = Random.Range(1, birds.Count + 1);
float dir = Random.value < 0.5f ? 1f : -1f;
float y0 = birdRow.y + Random.value * birdRow.h;
float sp = birdRow.speed * Random.Range(0.85f, 1.15f);
for (int i = 0; i < n; i++)
{
var b = birds[i];
b.on = true;
b.size = birdRow.w * Random.Range(0.85f, 1.05f);
b.x = dir > 0f ? -b.size - i * b.size * 1.3f : artW + b.size + i * b.size * 1.3f;
// 뒤따르는 새 = 위·아래 번갈아 조금씩(떼가 세로로 퍼지면 좌우 버튼 열 사이 틈에 걸림 · QA r1 M5)
b.y = y0 + (i % 2 == 1 ? -1f : 1f) * ((i + 1) / 2) * b.size * 0.3f;
b.vx = dir * sp;
b.ph = Random.value;
b.rt.gameObject.SetActive(true);
b.rt.localScale = new Vector3(dir, 1f, 1f); // 그림 = 오른쪽을 봄
}
nextFlock = t + Random.Range(birdRow.gapMin, birdRow.gapMax);
}
foreach (var b in birds)
{
if (!b.on) continue;
b.x += b.vx * dt;
float bob = birdRow.amp * Mathf.Sin((t / Mathf.Max(0.05f, birdRow.period) * 0.5f + b.ph) * Mathf.PI * 2f);
int f = (int)((t / Mathf.Max(0.05f, birdRow.period) + b.ph) * b.frames.Length) % b.frames.Length;
b.img.sprite = b.frames[f];
var sz = b.frames[f].rect.size;
float k = b.size / Mathf.Max(1f, sz.x);
b.rt.sizeDelta = sz * k * unit;
b.rt.anchoredPosition = new Vector2(b.x * unit, -(b.y + bob) * unit);
if ((b.vx > 0f && b.x > artW + b.size * 2f) || (b.vx < 0f && b.x < -b.size * 2f))
{
Hide(b);
nextFlock = Mathf.Max(nextFlock, t + Random.Range(birdRow.gapMin, birdRow.gapMax));
}
}
}
void UpdatePetals(float t, float dt, float unit)
{
if (petalRow == null || petals.Count == 0) return;
if (t >= nextPetal)
{
foreach (var p in petals)
{
if (p.on) continue;
p.on = true;
p.x = petalRow.x + Random.value * petalRow.w;
p.y = petalRow.y + Random.value * petalRow.h;
p.vy = petalRow.speed * Random.Range(0.8f, 1.2f);
p.vx = petalRow.speed * Random.Range(0.35f, 0.7f);
p.size = petalRow.scale * Random.Range(0.8f, 1.15f);
p.ph = Random.value;
p.t = 0f;
p.life = Mathf.Max(0.5f, petalRow.period * Random.Range(0.8f, 1.2f));
p.rot = Random.value * 360f;
p.vrot = Random.Range(-90f, 90f);
p.img.sprite = p.frames[Random.Range(0, p.frames.Length)];
p.rt.gameObject.SetActive(true);
break;
}
nextPetal = t + Random.Range(petalRow.gapMin, petalRow.gapMax);
}
foreach (var p in petals)
{
if (!p.on) continue;
p.t += dt;
if (p.t >= p.life)
{
Hide(p);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.vrot * dt;
float sway = petalRow.amp * Mathf.Sin((p.t * 1.3f + p.ph) * Mathf.PI * 2f);
var sz = p.img.sprite.rect.size;
float k = p.size / Mathf.Max(1f, Mathf.Max(sz.x, sz.y));
p.rt.sizeDelta = sz * k * unit;
p.rt.anchoredPosition = new Vector2((p.x + sway) * unit, -p.y * unit);
p.rt.localEulerAngles = new Vector3(0f, 0f, p.rot);
// 나타남(f_min 초) · 사라짐(f_max 초) — PD 2026-11-11 「짧게 떨어지는 느낌으로 보이다 투명하게 없어지도록」
float fin = Mathf.Max(0.05f, petalRow.min), fout = Mathf.Max(0.05f, petalRow.max);
float a = Mathf.Clamp01(Mathf.Min(p.t / fin, (p.life - p.t) / fout));
var c = p.img.color;
if (!Mathf.Approximately(c.a, a))
{
c.a = a;
p.img.color = c;
}
}
}
}
}