ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Scenario/BossIntro.cs

489 lines
24 KiB
C#
Raw Normal View History

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
/// <summary>
/// 보스 스테이지 시작 연출(2026-11-12 PD 「보스는 등장할 때 포커스 + 짤막한 대사(말풍선)」 · 「예시처럼 보스 등장 연출」 · 개발 요청서 C2).
/// ① 경고(위아래 검은 띠 + 붉은 가장자리 깜빡임) → ② 보스 포커스(카메라 이동·확대 · 다른 유닛 어둡게 · 발밑 붉은 원) →
/// ③ 컷인(위 「BOSS 등장!」 붉은 줄무늬 띠 → 보스 전신 + 아래 이름 띠) → ④ 말풍선 대사(타자 효과 · 2인 보스는 n_order 순) → ⑤ 아군 시야로 복귀.
/// 값 = ScenarioData.Cfg(boss_intro_config.csv) · 대사 = boss_intro.csv · 그림 = Resources/Scenario/*.png(띠·글자·말풍선 · 없으면 색 면) + 보스 유닛 그림.
/// 연출 동안 전투 단계 = Setup(아군 조작 잠금) · 끝나면 onDone(아군 차례). Sim.Active(적 AI 가상 실행) 중에는 아무것도 하지 않는다.
/// </summary>
public class BossIntro : MonoBehaviour
{
static BossIntro current;
BattleManager battle;
CameraController cc;
Action onDone;
List<Unit> bosses = new List<Unit>();
List<ScenarioData.BossLine> lines;
bool withCutin;
bool skip;
int taps;
Vector3 camStart;
float viewStart;
readonly Dictionary<SpriteRenderer, Color> dimmed = new Dictionary<SpriteRenderer, Color>();
readonly List<GameObject> rings = new List<GameObject>();
RectTransform root, fx;
Image dim, edge, barTop, barBottom;
Button skipBtn;
static float C(string k)
{
return ScenarioData.Cfg(k);
}
/// <summary>이 스테이지에 연출을 틀지(보스 행 있음 · 가상 실행 아님 · 다시 출정 = replay_mode 2 면 안 틂).</summary>
public static bool Wants(BalanceConfig.StageDef stage, bool cleared)
{
if (Sim.Active || stage == null || stage.kind != StageKind.Boss) return false;
if (ScenarioData.Bosses(stage.chapter, stage.stage).Count == 0) return false;
return !(cleared && Mathf.RoundToInt(C("replay_mode")) == 2);
}
/// <summary>연출 시작 — cleared = 이미 깬 스테이지(replay_mode 1 = 컷인 빼고 ②④⑤).</summary>
public static void Begin(BattleManager battle, BalanceConfig.StageDef stage, bool cleared, Action onDone)
{
Abort();
var cc = FindFirstObjectByType<CameraController>();
if (cc == null || cc.Cam == null)
{
onDone?.Invoke();
return;
}
var go = new GameObject("BossIntroUI");
var bi = go.AddComponent<BossIntro>();
current = bi;
bi.battle = battle;
bi.cc = cc;
bi.onDone = onDone;
bi.lines = ScenarioData.Bosses(stage.chapter, stage.stage);
bi.withCutin = !(cleared && Mathf.RoundToInt(C("replay_mode")) == 1);
foreach (var l in bi.lines)
{
Unit hit = null;
foreach (var u in battle.AliveUnits(Faction.Enemy))
if (u.IsBoss && u.ArtId == l.id) { hit = u; break; }
if (hit == null)
foreach (var u in battle.AliveUnits(Faction.Enemy))
if (u.IsBoss && !bi.bosses.Contains(u)) { hit = u; break; }
bi.bosses.Add(hit);
}
bi.Build();
bi.StartCoroutine(bi.Run());
}
/// <summary>진행 중 연출을 바로 치움(전투 재시작 · 월드맵 복귀) — 어둡게·원·카메라 상태를 되돌린다.</summary>
public static void Abort()
{
if (current == null) return;
current.Restore();
Destroy(current.gameObject);
current = null;
}
// ---------- 화면 ----------
static Sprite Spr(string name)
{
return Resources.Load<Sprite>(ScenarioData.Root + name);
}
static RectTransform Rt(string name, Transform parent, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
var rt = (RectTransform)go.transform;
rt.anchorMin = aMin;
rt.anchorMax = aMax;
rt.pivot = pivot;
rt.anchoredPosition = pos;
rt.sizeDelta = size;
return rt;
}
static Image Img(string name, Transform parent, Sprite sp, Color col, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{
var img = Rt(name, parent, aMin, aMax, pivot, pos, size).gameObject.AddComponent<Image>();
img.sprite = sp;
img.color = col;
img.raycastTarget = false;
return img;
}
static Image Full(string name, Transform parent, Sprite sp, Color col)
{
return Img(name, parent, sp, col, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
}
static Text Txt(Transform parent, string s, int size, Color col, TextAnchor anchor, Vector2 pos, Vector2 box, Font font)
{
var t = Rt("Text", parent, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), pos, box).gameObject.AddComponent<Text>();
t.font = font;
t.fontSize = size;
t.color = col;
t.alignment = anchor;
t.text = s;
t.supportRichText = false;
t.raycastTarget = false;
t.horizontalOverflow = HorizontalWrapMode.Wrap;
t.verticalOverflow = VerticalWrapMode.Overflow;
return t;
}
static void Stroke(Text t, Color col, float d)
{
var o = t.gameObject.AddComponent<Outline>();
o.effectColor = col;
o.effectDistance = new Vector2(d, -d);
var o2 = t.gameObject.AddComponent<Outline>();
o2.effectColor = col;
o2.effectDistance = new Vector2(-d, d);
}
void Build()
{
UiLayer.Root(gameObject, cc.Cam, 460, 0.38f, UiLayer.Reference, UiRate.Event); // 아웃게임 위 덮개(재화 바 · 450)보다 위 · 연출 동안 화면 전체를 덮음
root = (RectTransform)transform;
// 누름 받이(탭 1 = 대사 전부 · 탭 2 = 닫기) — 전체 화면 · 투명
var catcher = Full("Tap", root, null, new Color(0f, 0f, 0f, 0f));
catcher.raycastTarget = true;
catcher.gameObject.AddComponent<Button>().onClick.AddListener(() => taps++);
// 움직이는 것 = Frame 층 하나에 모음(UI 캔버스 규칙)
fx = UiLayer.Group("Fx", root, UiRate.Frame);
dim = Full("Dim", fx, null, new Color(0f, 0f, 0f, 0f));
edge = Full("Edge", fx, Spr("warn_edge"), new Color(1f, 1f, 1f, 0f));
float bar = UiLayer.Reference.y * C("letterbox_ratio");
barTop = Img("BarTop", fx, null, Color.black, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(0f, bar));
barBottom = Img("BarBottom", fx, null, Color.black, new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(0f, bar));
// 건너뛰기(오른쪽 위 · skip_show_after_sec 뒤 보임)
var sb = Img("Skip", root, null, new Color(0f, 0f, 0f, 0.6f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-40f, -60f), new Vector2(190f, 80f));
sb.raycastTarget = true;
skipBtn = sb.gameObject.AddComponent<Button>();
skipBtn.onClick.AddListener(() => skip = true);
Txt(sb.transform, "건너뛰기 >>", 34, Color.white, TextAnchor.MiddleCenter, Vector2.zero, new Vector2(190f, 80f), UISkin.TitleFont);
sb.gameObject.SetActive(false);
}
// ---------- 흐름 ----------
IEnumerator Run()
{
camStart = cc.transform.position;
viewStart = cc.ViewHeight;
StartCoroutine(SkipButtonLater());
yield return Warn();
if (!skip) yield return Focus(Target(0), C("focus_move_sec"), true);
if (!skip && withCutin) yield return Cutin();
for (int i = 0; i < lines.Count && !skip; i++)
{
if (i > 0) yield return Focus(Target(i), C("second_boss_move_sec"), false);
if (!skip) yield return Bubble(i);
}
yield return Return();
Finish();
}
IEnumerator SkipButtonLater()
{
float t = 0f, show = C("skip_show_after_sec");
while (t < show) { t += Time.unscaledDeltaTime; yield return null; }
if (skipBtn != null) skipBtn.gameObject.SetActive(true);
}
Vector2 Target(int i)
{
var u = i < bosses.Count ? bosses[i] : null;
return u != null ? u.Position : (Vector2)camStart;
}
static float Ease(float k)
{
k = Mathf.Clamp01(k);
return k * k * (3f - 2f * k);
}
IEnumerator Warn()
{
float dur = C("warn_sec"), bar = barTop.rectTransform.sizeDelta.y;
int blinks = Mathf.Max(1, Mathf.RoundToInt(C("warn_blink_count")));
for (float t = 0f; t < dur && !skip; t += Time.unscaledDeltaTime)
{
float k = t / dur;
float y = bar * Ease(k);
barTop.rectTransform.anchoredPosition = new Vector2(0f, -y);
barBottom.rectTransform.anchoredPosition = new Vector2(0f, y);
edge.color = new Color(1f, 1f, 1f, Mathf.Abs(Mathf.Sin(k * blinks * Mathf.PI)));
yield return null;
}
barTop.rectTransform.anchoredPosition = new Vector2(0f, -bar);
barBottom.rectTransform.anchoredPosition = new Vector2(0f, bar);
edge.color = new Color(1f, 1f, 1f, 0f);
}
IEnumerator Focus(Vector2 target, float dur, bool first)
{
Vector2 p0 = cc.transform.position;
float h0 = cc.ViewHeight, h1 = viewStart * C("focus_view_ratio");
if (first) DimAndRing();
for (float t = 0f; t < dur && !skip; t += Time.unscaledDeltaTime)
{
float k = Ease(t / dur);
cc.SetViewHeight(Mathf.Lerp(h0, h1, k));
cc.SnapTo(Vector2.Lerp(p0, target, k));
yield return null;
}
cc.SetViewHeight(h1);
cc.SnapTo(target);
}
void DimAndRing()
{
float d = C("dim_others");
var all = new List<Unit>(battle.AliveUnits(Faction.Player));
all.AddRange(battle.AliveUnits(Faction.Enemy));
foreach (var u in all)
{
if (bosses.Contains(u)) continue;
foreach (var sr in u.GetComponentsInChildren<SpriteRenderer>())
{
if (dimmed.ContainsKey(sr)) continue;
dimmed[sr] = sr.color;
var c = sr.color;
sr.color = new Color(c.r * (1f - d), c.g * (1f - d), c.b * (1f - d), c.a);
}
}
foreach (var b in bosses)
{
if (b == null) continue;
var ring = ProceduralSprites.NewSprite(b.transform, "BossRing", ProceduralSprites.Ring, new Color(0.9f, 0.15f, 0.1f, 0.9f), b.Radius * 2f * C("focus_ring_scale"), -1);
rings.Add(ring.gameObject);
}
}
IEnumerator Cutin()
{
var g = Rt("Cutin", fx, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
var bandTopSp = Spr("cutin_band_top");
var bandBotSp = Spr("cutin_band_bottom");
float W = UiLayer.Reference.x;
float topH = bandTopSp != null ? W * bandTopSp.rect.height / bandTopSp.rect.width : 420f;
float botH = bandBotSp != null ? W * bandBotSp.rect.height / bandBotSp.rect.width : 460f;
var top = Img("BandTop", g, bandTopSp, bandTopSp != null ? Color.white : new Color(0.7f, 0.1f, 0.08f), new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f), new Vector2(-W, 0f), new Vector2(0f, topH));
// 「BOSS 등장!」 = 위 띠와 같은 크기의 그림(글자 자리 그대로) · 피벗 = 그림의 피벗(글자 가운데) → 찍힘 확대가 글자 가운데 기준
var titleSp = Spr("cutin_title");
var title = Full("Title", top.transform, titleSp, titleSp != null ? Color.white : new Color(1f, 0.8f, 0.3f, 0f));
if (titleSp != null)
{
var piv = new Vector2(titleSp.pivot.x / titleSp.rect.width, titleSp.pivot.y / titleSp.rect.height);
title.rectTransform.pivot = piv;
title.rectTransform.anchoredPosition = Vector2.zero;
title.rectTransform.offsetMin = title.rectTransform.offsetMax = Vector2.zero;
}
var bottom = Img("BandBottom", g, bandBotSp, bandBotSp != null ? Color.white : new Color(0.6f, 0.08f, 0.06f), new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0f), new Vector2(W, 0f), new Vector2(0f, botH));
// 보스 그림(1인 = cutin_art_height · 2인 = 좌우 각 cutin_art_height_duo)
int n = Mathf.Max(1, bosses.Count);
float artH = UiLayer.Reference.y * (n > 1 ? C("cutin_art_height_duo") : C("cutin_art_height"));
var arts = new List<RectTransform>();
var names = new List<string>();
var titles = new List<string>();
for (int i = 0; i < bosses.Count; i++)
{
var u = bosses[i];
names.Add(u != null ? u.DisplayName : lines[i].id);
titles.Add(lines[i].title);
Sprite sp = u != null ? Resources.Load<Sprite>("Units/" + u.ArtId + "/front_action/f000") : null;
float aw = sp != null ? artH * sp.rect.width / sp.rect.height : artH * 0.8f;
float cx = n > 1 ? (i == 0 ? -W * 0.22f : W * 0.22f) : 40f;
var art = Img("Art" + i, g, sp, sp != null ? Color.white : new Color(0.85f, 0.3f, 0.2f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(cx + W, 60f), new Vector2(aw, artH));
arts.Add(art.rectTransform);
}
// 아래 띠 글자 = 칭호(작게) · 이름(크게) · WARNING!!!(그림)
// 이름 묶음 자리 = 아래 띠 그림의 이름 칸(그림 아래에서 47 % · 가운데에서 왼쪽 20 · 부품 그림 기준 배치)
var info = Rt("Info", bottom.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-20f, botH * 0.47f), new Vector2(W, botH));
info.localRotation = Quaternion.Euler(0f, 0f, C("cutin_band_angle"));
var tt = Txt(info, string.Join(" · ", titles.ToArray()), 52, new Color(1f, 0.92f, 0.82f), TextAnchor.MiddleCenter, new Vector2(0f, 118f), new Vector2(W, 70f), UISkin.TitleFont);
Stroke(tt, new Color(0.31f, 0.05f, 0.04f), 3f);
var nm = Txt(info, string.Join(" · ", names.ToArray()), n > 1 ? 120 : 150, Color.white, TextAnchor.MiddleCenter, new Vector2(0f, 10f), new Vector2(W, 170f), UISkin.TitleFont);
Stroke(nm, new Color(0.47f, 0.04f, 0.04f), 6f);
var sh = nm.gameObject.AddComponent<Shadow>();
sh.effectColor = new Color(0.9f, 0.16f, 0.08f, 0.8f);
sh.effectDistance = new Vector2(0f, -8f);
var warnSp = Spr("cutin_warning");
var warn = Img("Warning", info, warnSp, warnSp != null ? Color.white : new Color(1f, 0.85f, 0.4f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -110f), warnSp != null ? new Vector2(warnSp.rect.width, warnSp.rect.height) * 0.9f : new Vector2(360f, 60f));
// 들어옴 — 어둡게 → 위 띠(왼쪽 밖에서) + 글자 찍힘 → 지연 → 보스(오른쪽에서)·아래 띠
float dark = C("cutin_bg_dark"), bIn = C("cutin_banner_in_sec"), pop = C("cutin_banner_pop_scale");
for (float t = 0f; t < bIn && !skip; t += Time.unscaledDeltaTime)
{
float k = Ease(t / bIn);
dim.color = new Color(0f, 0f, 0f, dark * k);
top.rectTransform.anchoredPosition = new Vector2(Mathf.Lerp(-W, 0f, k), 0f);
title.rectTransform.localScale = Vector3.one * Mathf.Lerp(pop, 1f, k);
yield return null;
}
dim.color = new Color(0f, 0f, 0f, dark);
top.rectTransform.anchoredPosition = Vector2.zero;
title.rectTransform.localScale = Vector3.one;
yield return Wait(C("cutin_info_delay_sec"));
float aIn = C("cutin_in_sec");
for (float t = 0f; t < aIn && !skip; t += Time.unscaledDeltaTime)
{
float k = Ease(t / aIn);
for (int i = 0; i < arts.Count; i++)
{
float cx = n > 1 ? (i == 0 ? -W * 0.22f : W * 0.22f) : 40f;
arts[i].anchoredPosition = new Vector2(Mathf.Lerp(cx + W, cx, k), 60f);
}
bottom.rectTransform.anchoredPosition = new Vector2(Mathf.Lerp(W, 0f, k), 0f);
yield return null;
}
for (int i = 0; i < arts.Count; i++) arts[i].anchoredPosition = new Vector2(n > 1 ? (i == 0 ? -W * 0.22f : W * 0.22f) : 40f, 60f);
bottom.rectTransform.anchoredPosition = Vector2.zero;
cc.Shake(C("cutin_shake_sec"), C("cutin_shake"));
yield return Wait(C("cutin_hold_sec"));
// 빠짐 — 왼쪽 위로 밀려 나가며 흐려짐
var cg = g.gameObject.AddComponent<CanvasGroup>();
float outT = C("cutin_out_sec");
for (float t = 0f; t < outT && !skip; t += Time.unscaledDeltaTime)
{
float k = Ease(t / outT);
cg.alpha = 1f - k;
g.anchoredPosition = new Vector2(-W * 0.25f * k, 0f);
dim.color = new Color(0f, 0f, 0f, dark * (1f - k));
yield return null;
}
dim.color = new Color(0f, 0f, 0f, 0f);
Destroy(g.gameObject);
}
IEnumerator Bubble(int i)
{
var u = i < bosses.Count ? bosses[i] : null;
var l = lines[i];
if (u == null || string.IsNullOrEmpty(l.line)) yield break;
float fs = C("bubble_font_size");
var bub = Img("Bubble", fx, Spr("bubble"), Color.white, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0f), Vector2.zero, new Vector2(640f, fs * 2f * 1.35f + 80f));
bub.type = Image.Type.Sliced;
var tail = Img("Tail", bub.transform, Spr("bubble_tail"), Color.white, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 1f), new Vector2(0f, 4f), new Vector2(54f, 40f));
var tag = Img("NameTag", bub.transform, Spr("name_tag"), new Color(0.75f, 0.16f, 0.12f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 0.5f), new Vector2(24f, 0f), new Vector2(0f, 52f));
tag.type = Image.Type.Sliced;
var tagText = Txt(tag.transform, u.DisplayName, 30, Color.white, TextAnchor.MiddleCenter, Vector2.zero, new Vector2(200f, 52f), UISkin.TitleFont);
tag.rectTransform.sizeDelta = new Vector2(tagText.preferredWidth + 44f, 52f);
tagText.rectTransform.sizeDelta = tag.rectTransform.sizeDelta;
var body = Txt(bub.transform, "", Mathf.RoundToInt(fs), new Color(0.24f, 0.13f, 0.07f), TextAnchor.MiddleCenter, new Vector2(0f, -4f), new Vector2(580f, fs * 2f * 1.35f + 20f), UISkin.TitleFont);
float popT = C("bubble_pop_sec"), from = C("bubble_pop_from"), peak = C("bubble_pop_peak");
taps = 0;
for (float t = 0f; t < popT && !skip; t += Time.unscaledDeltaTime)
{
float k = t / popT;
bub.rectTransform.localScale = Vector3.one * (k < 0.7f ? Mathf.Lerp(from, peak, k / 0.7f) : Mathf.Lerp(peak, 1f, (k - 0.7f) / 0.3f));
Place(bub.rectTransform, tail.rectTransform, u);
yield return null;
}
bub.rectTransform.localScale = Vector3.one;
float cps = Mathf.Max(1f, C("bubble_char_per_sec"));
float shown = 0f;
while (shown < l.line.Length && !skip && taps == 0)
{
shown += cps * Time.unscaledDeltaTime;
body.text = l.line.Substring(0, Mathf.Min(l.line.Length, Mathf.FloorToInt(shown)));
Place(bub.rectTransform, tail.rectTransform, u);
yield return null;
}
body.text = l.line;
if (taps > 0) taps = 0; // 탭 1 = 전부 표시 · 다음 탭 = 닫기
float hold = C("bubble_hold_sec");
for (float t = 0f; t < hold && !skip && taps == 0; t += Time.unscaledDeltaTime)
{
Place(bub.rectTransform, tail.rectTransform, u);
yield return null;
}
var cg = bub.gameObject.AddComponent<CanvasGroup>();
float fade = C("bubble_fade_sec");
for (float t = 0f; t < fade && !skip; t += Time.unscaledDeltaTime)
{
cg.alpha = 1f - t / fade;
yield return null;
}
Destroy(bub.gameObject);
}
/// <summary>말풍선 = 보스 머리 위 · 화면 밖이면 안쪽으로 밀어 넣고 꼬리만 보스를 따라감.</summary>
void Place(RectTransform bub, RectTransform tail, Unit u)
{
var cam = cc.Cam;
Vector2 head = u.Position + new Vector2(0f, u.Radius * 1.6f);
Vector2 screen = RectTransformUtility.WorldToScreenPoint(cam, head);
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(root, screen, cam, out local);
var half = root.rect.size * 0.5f;
var size = bub.sizeDelta;
float x = Mathf.Clamp(local.x, -half.x + size.x * 0.5f + 20f, half.x - size.x * 0.5f - 20f);
float y = Mathf.Clamp(local.y + 40f, -half.y + 40f, half.y - size.y - 40f);
var p = new Vector2(x, y);
if (bub.anchoredPosition != p) bub.anchoredPosition = p;
var tp = new Vector2(Mathf.Clamp(local.x - x, -size.x * 0.5f + 40f, size.x * 0.5f - 40f), 4f);
if (tail.anchoredPosition != tp) tail.anchoredPosition = tp;
}
IEnumerator Return()
{
Restore();
float dur = C("return_sec");
Vector2 p0 = cc.transform.position;
float h0 = cc.ViewHeight, bar = barTop.rectTransform.sizeDelta.y;
float a0 = dim.color.a;
for (float t = 0f; t < dur; t += Time.unscaledDeltaTime)
{
float k = Ease(t / dur);
cc.SetViewHeight(Mathf.Lerp(h0, viewStart, k));
cc.SnapTo(Vector2.Lerp(p0, camStart, k));
barTop.rectTransform.anchoredPosition = new Vector2(0f, -bar * (1f - k));
barBottom.rectTransform.anchoredPosition = new Vector2(0f, bar * (1f - k));
dim.color = new Color(0f, 0f, 0f, a0 * (1f - k));
yield return null;
}
cc.SetViewHeight(viewStart);
}
IEnumerator Wait(float sec)
{
for (float t = 0f; t < sec && !skip; t += Time.unscaledDeltaTime) yield return null;
}
void Restore()
{
foreach (var kv in dimmed)
if (kv.Key != null) kv.Key.color = kv.Value;
dimmed.Clear();
foreach (var r in rings)
if (r != null) Destroy(r);
rings.Clear();
}
void Finish()
{
var done = onDone;
onDone = null;
if (current == this) current = null;
Destroy(gameObject);
done?.Invoke();
}
void OnDestroy()
{
Restore();
if (current == this) current = null;
}
}
}