489 lines
24 KiB
C#
489 lines
24 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|