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

333 lines
14 KiB
C#
Raw Normal View History

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;
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
List<LobbyScene.Row> src;
List<LobbyScene.Row> All(string kind)
{
if (src == null) return LobbyScene.All(kind);
var list = new List<LobbyScene.Row>();
foreach (var r in src)
if (r.kind == kind)
list.Add(r);
return list;
}
LobbyScene.Row First(string kind)
{
var l = All(kind);
return l.Count > 0 ? l[0] : null;
}
/// <summary>영웅 자리 층(그림을 꽉 채움 · 그림 좌표 = 정규화 앵커) — 뒤 연출(깃발·불빛)과 앞 연출(새·꽃잎) 사이.</summary>
public RectTransform Lineup { get; private set; }
/// <summary>
/// 그림(art) 위에 장면 층(Frame 캔버스 1장)을 만든다 — 순서 = Back(깃발·불빛) → Lineup(그림자·영웅 · 대기 프레임 재생) → Front(새·꽃잎 · PD 2026-11-11 「벚꽃잎은 유닛보다 앞」).
/// 영웅 대기 프레임이 매 프레임 바뀌므로 영웅도 이 Frame 층에 둔다(화면 캔버스 = Event 그대로 · P41).
/// </summary>
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
public static LobbyAmbient Build(RectTransform art, float artW, float artH, List<LobbyScene.Row> rows = null)
{
var layer = UiLayer.Group("Ambient", art, UiRate.Frame);
var a = layer.gameObject.AddComponent<LobbyAmbient>();
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
a.src = rows; // null = 로비 표(lobby_scene.csv) · 내정 = 그 탭 줄(DomesticScene.Page)
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);
타이틀 화면 — 장수 모임 등장 · R22 대기 모션 · 꾸미기 연출 · Press Any Key(PD 2026-11-13 확정) - OutGame/UI/TitleScreen.cs: BootFlow.TitleProvider 등록(메모리 세이브 = 테스트·캡처는 건너뜀 → 기존처럼 바로 로비) 장수 6명이 화면 밖에서 뒷줄→앞줄로 모임(착지 흙먼지) → 로고 내려앉음·둥실·반짝이 → Press Any Key 깜박임 누름 = 등장 중이면 건너뜀 · 글자가 뜬 뒤면 검정 페이드 → 로비 · 타이틀 동안 아래 루트 캔버스 끔 층(P41) = 루트 Static(배경 · cover) → Ambient Frame(햇살·빛 번짐·흙먼지·불씨) → Stage Frame(장수·로고·글자 · contain) - 표 Resources/UI/title_screen.csv(자리·등장 박자·대기 유지 초·깜빡임·로고·글자·페이드) · title_ambient.csv(LobbyAmbient) - 그림 Resources/UI/Title/(배경 · 로고 A · 장수 6 × R22 자동 리그 4자세+깜빡임 · pack) · 아틀라스 UI_Title(ui_atlas.csv folder 1줄) - LobbyAmbient.Load: 테마에 없는 id 가 경로면 Resources 폴백(1줄) - 캡처 TitleCaptureDriver + Editor/TitleCapture(-executeMethod TKS.EditorTools.TitleCapture.Capture -titleDir) · 테스트 TitleScreenTests 8 - 검증: 에디터 TitleScreenTests 8/8 · BootFlowTests 4/4 · OutGameTests.Lobby_Tabs_Back_ComingSoon 1/1 · QA r1 B4(화면비) → r2 PASS(B 0) - 의존: BootFlow.cs·LoadingScreen.cs(로딩 화면 세션 · 아직 미커밋) — 그 커밋 뒤 push Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 03:50:04 +00:00
if (sp == null && id.IndexOf('/') >= 0) sp = Resources.Load<Sprite>(id); // 타이틀 연출 = Resources 경로(UI/Title/pack/… · 테마 등록 없이 · 2026-11-13)
if (sp != null) list.Add(sp);
}
return list.ToArray();
}
void Setup()
{
// 깃발(원래 깃발 위에 같은 자리 · UiWaveImage)
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
foreach (var r in 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;
}
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
// 풀·꽃 흔들림(내정 연병장 · PD 2026-11-12) = 깃발과 같은 격자 · 아래끝 고정 · i_n 1 = 좌우 뒤집음
foreach (var r in All("sway"))
{
var sp = Load(r);
if (sp.Length == 0) continue;
var rt = ArtRect("Sway_" + 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.fromBottom = true;
wv.rows = 6;
wv.cols = 3;
wv.amp = r.amp;
wv.period = r.period;
wv.waves = r.waves;
wv.phase = r.phase;
wv.shade = r.shade >= 0f ? r.shade : 0f;
wv.color = r.color;
if (r.n == 1) rt.localScale = new Vector3(-1f, 1f, 1f);
}
// 불빛(관청 창 · 문 앞 번짐 · 석등) = 알파 깜박임
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
foreach (var r in 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 });
}
내정 메뉴 개편(훈련·순찰·개발 배경 장면) + 「책사 → 승병」 게임 전체 - 3탭 배경 = 로비 화풍(연병장·저잣거리·규장각) · 장면 표 Resources/UI/domestic_scene.csv · 연출 LobbyAmbient(Frame 캔버스) - 훈련: 병종별 범용 병사 훈련 동작(UiDrillAnim · 뒷발 고정 · 병종별 박자 · 화면 배율) · 궁병 훈련 전용 클립 drill_ready/drill_action(대기 원화 리그) + 날아가는 화살 · 병종 탭 아이콘 · 카드 = 이름 +수치% - 순찰: 정보 판 1장(시계 + 쌓인 시간 · 게이지 · 시간당 골드·경험치·전리품 오른쪽 정렬 · 금테 안 획득 보상) · 버튼 수확/즉시 수확 · 광고 2배 = 수확 팝업 · 문 앞 순찰 영웅 + 창병 2 · 상인·주민 NPC 5(R22 대기 모션 · 길바닥) - 개발: 양피지 두루마리 위 성장 트리(노드 그림 아이콘 27) · 재화 칩 · 아래 두루마리 = 영웅 배치 + 트리 초기화 - 영웅 파견 3자리(훈련 병종 지휘관 · 순찰 · 개발 · 장면 표시만) · 세이브 필드 3 - 병사 5종 자세·타격 그림 색·크기 대기에 맞춤(전투 그림도 같은 파일) - 승병: UnitTypeInfo 이름/표시 글자 · heroes 병사 3 이름·한자 · 스킬·기믹 설명 · 튜토리얼 팁 + 임포트 자산(HeroDb·SkillInfoDb·GimmickDb·StageGimmickTable) - 테스트 DomesticSceneTests 4 · BattleFlowTests 표시 글자 PD 지시 2026-11-12(대화로그 공유/대화로그/삼국지슬링샷/2026-11-12_내정메뉴_개편.md · 레시피 R23) · QA r1~r12 · 마지막 r11·r12 PASS(B 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:41:33 +00:00
birdRow = First("bird");
petalRow = 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;
}
}
}
}
}