808 lines
60 KiB
C#
808 lines
60 KiB
C#
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
using VS.Sim;
|
||
using Vector2 = UnityEngine.Vector2;
|
||
using Vector3 = UnityEngine.Vector3;
|
||
|
||
// 전투 탭 — 클래시 로얄 화면 구성(위 적 성 · 무작위 맵 · 아래 카드 칸 + 마나 막대 · 오른쪽 위 = 상대방 얼굴 + 스킬 정보)
|
||
// 화면 비율 = 클래시 로얄(맵이 화면 폭을 채움 · 비스듬히 내려다봐 칸 세로가 짧음 · 유닛 키 약 1.5칸) — 수치 = battle.json view
|
||
// 카드를 누르면 아래 가운데에서 바로 소환 · 맵을 누르면 깃발 → 내 유닛이 깃발을 거쳐 성으로 · 왼쪽 위 「시험」 = 맵·성 투사체·기믹을 골라 다음 전투에 적용
|
||
namespace VS.View
|
||
{
|
||
public partial class GameApp
|
||
{
|
||
public const float TabH = 150f;
|
||
public RectTransform StreamRoot, BattleRoot;
|
||
public int Tab; // 0 방송 · 1 전투
|
||
readonly List<(Image bg, Text label)> tabs = new List<(Image, Text)>();
|
||
|
||
public Battle B;
|
||
public BattleSetup BattleTest; // 시험 설정 (null = 단계 기본 · 무작위 맵)
|
||
RectTransform arena, unitLayer, fxLayerB, castleHpBar;
|
||
Image arenaImg;
|
||
BViewDef V => T.Battle.view;
|
||
Image castleImg, castleHpFill, manaFill, flagImg;
|
||
Text castleHpText, stageText, manaText, manaMaxText, infoText;
|
||
float flagPop;
|
||
// 상대방(여성 캐릭터) 얼굴 + 스킬 정보 — 전투마다 성 구성에 맞춰 줄을 새로 만든다
|
||
RectTransform rivalBox; Image rivalFrame, rivalFace;
|
||
class RivalRow { public RectTransform rt; public Image bg, fill; public Text info; public BWeapon w; public BSummoner s; public float lastCd, flash; }
|
||
readonly List<RivalRow> rivalRows = new List<RivalRow>();
|
||
const float RivalW = 240f, RivalTop = 116f, RivalRowH = 88f; // 클래시 로얄 남은 시간 칸 자리 — 맵을 덜 가리게 작게
|
||
RectTransform overlay; Image overlayRibbon; Text overlayTitle, overlayInfo, overlayBtnLabel;
|
||
readonly List<(Button b, Image face, Text cost, Image lockImg, Text lockText)> cards = new List<(Button, Image, Text, Image, Text)>();
|
||
readonly Dictionary<BUnit, (RectTransform rt, Image hp, Image stun)> unitViews = new Dictionary<BUnit, (RectTransform, Image, Image)>();
|
||
readonly Dictionary<BMinion, (RectTransform rt, Image hp)> minionViews = new Dictionary<BMinion, (RectTransform, Image)>();
|
||
readonly List<Image> shotViews = new List<Image>(), fxViews = new List<Image>();
|
||
readonly List<(Transform t, float key)> depth = new List<(Transform, float)>();
|
||
bool rewardGiven;
|
||
|
||
// ---------------- 탭(로비 아래 메뉴) = 연구 | 전투 | 유닛 (2026-10-29 PD) · 연구 = 방송 화면 · 전투 중에는 숨김 ----------------
|
||
RectTransform tabBar;
|
||
|
||
void BuildTabs()
|
||
{
|
||
var bar = UI.Img(Root, "TabBar", Skin.TabBar, Skin.On ? Pal.Navy : (Color)new Color32(36, 26, 52, 255), true);
|
||
tabBar = bar.rectTransform;
|
||
tabBar.Place(new Vector2(0, 0), new Vector2(1, 0), Vector2.zero, new Vector2(0, TabH), new Vector2(0.5f, 0));
|
||
var hl = bar.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = true; hl.padding = new RectOffset(20, 20, Skin.On ? 0 : 12, Skin.On ? 6 : 12); hl.spacing = 16;
|
||
string[] names = { L("UI.Tab.Research", "연구"), L("UI.Tab.Battle", "전투"), L("UI.Tab.Units", "유닛") };
|
||
string[] icons = { "skill", "pk", "follower" };
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
int idx = i;
|
||
var b = UI.Btn(bar.transform, "Tab" + i, Skin.On ? Skin.TabFocus : Art.Round, Pal.Purple, () => SwitchTab(idx));
|
||
UI.Icon(b.transform, "Icon", Skin.Icon(icons[i]), Color.white).rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(-68, 0), new Vector2(80, 80));
|
||
var t = UI.Txt(b.transform, "Label", names[i], 42, Color.white, TextAnchor.MiddleCenter, true);
|
||
t.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(42, 0), new Vector2(150, 80));
|
||
tabs.Add((b.GetComponent<Image>(), t));
|
||
}
|
||
SwitchTab(0);
|
||
}
|
||
|
||
public void SwitchTab(int t)
|
||
{
|
||
Tab = t;
|
||
// 전투 탭 = 싸우는 중이면 전투 화면 · 아니면 스테이지 선택(다음에 깰 스테이지부터) · 유닛 탭 = 덱 + 유닛 목록
|
||
bool fighting = B != null && B.State == BattleState.Fighting;
|
||
if (StreamRoot != null) StreamRoot.gameObject.SetActive(t == 0);
|
||
if (BattleRoot != null) BattleRoot.gameObject.SetActive(t == 1 && fighting);
|
||
if (StageRoot != null) StageRoot.gameObject.SetActive(t == 1 && !fighting);
|
||
if (UnitRoot != null) UnitRoot.gameObject.SetActive(t == 2);
|
||
for (int i = 0; i < tabs.Count; i++) tabs[i].bg.color = Skin.On ? (i == t ? Pal.NavyLight : new Color(0, 0, 0, 0)) : (i == t ? Pal.Pink : (Color)new Color32(84, 64, 120, 255));
|
||
if (t == 1 && !fighting) ShowStage(G.StageFocus);
|
||
if (t == 2) OpenUnits();
|
||
UpdateTabBar();
|
||
CloseAllPopups();
|
||
}
|
||
|
||
/// 전투 화면이 떠 있으면 아래 메뉴를 숨긴다(클래시 로얄처럼 전투는 화면 전체)
|
||
void UpdateTabBar() { if (tabBar != null) tabBar.gameObject.SetActive(BattleRoot == null || !BattleRoot.gameObject.activeSelf); }
|
||
|
||
// ---------------- 아레나 그림 (전투마다 맵에 맞춰 새로) ----------------
|
||
// 칸 → 화면 px: 가로 tileX · 세로 tileY (클래시 로얄처럼 비스듬히 내려다봐 땅의 세로가 짧다 · 땅 위 원 = 타원)
|
||
Vector2 TileToLocal(Vector2 tp) => new Vector2((tp.x - B.C.arenaW * 0.5f) * V.tileX, (tp.y - B.C.arenaH * 0.5f) * V.tileY);
|
||
Vector2 TileVec(Vector2 d) => new Vector2(d.x * V.tileX, d.y * V.tileY);
|
||
|
||
/// 맵 그림 = 좌우 sideTiles 칸 맵 밖 장식(짙은 풀 + 돌 턱 · 강은 끝까지) + 가운데 맵 · 칸당 텍스처 S px → 화면에서 칸 가로·세로 비율대로 늘어남
|
||
Sprite ArenaSprite(Battle b)
|
||
{
|
||
var c = b.C; var m = b.Map; const float S = 30f;
|
||
float ox = c.view.sideTiles * S;
|
||
int W = Mathf.RoundToInt((c.arenaW + 2f * c.view.sideTiles) * S), H = Mathf.RoundToInt(c.arenaH * S);
|
||
var r = new Raster(W, H);
|
||
Color32 g0 = new Color32(96, 180, 110, 255), g1 = new Color32(88, 170, 102, 255), e0 = new Color32(66, 132, 78, 255), e1 = new Color32(60, 124, 72, 255);
|
||
for (int y = 0; y < H; y++)
|
||
for (int x = 0; x < W; x++)
|
||
{
|
||
float tx = (x - ox) / S;
|
||
int k = (Mathf.FloorToInt(tx) + (int)(y / S)) & 1;
|
||
r.Px[y * W + x] = tx >= 0f && tx < c.arenaW ? (k == 0 ? g0 : g1) : (k == 0 ? e0 : e1);
|
||
}
|
||
// 맵 가장자리 돌 턱(클래시 로얄 아레나 옆 벽처럼 — 유닛이 못 나가는 곳)
|
||
Color curbDk = new Color32(104, 100, 116, 255), curb = new Color32(156, 152, 166, 255);
|
||
foreach (float ex in new[] { ox, W - ox })
|
||
{
|
||
r.RRect(ex - S * 0.3f, -4, ex + S * 0.3f, H + 4, 2, curbDk);
|
||
r.RRect(ex - S * 0.18f, -4, ex + S * 0.18f, H + 4, 2, curb);
|
||
}
|
||
// 성 앞마당 · 소환 자리(아래 가운데 — 내 진영은 평지)
|
||
r.Circle(ox + m.Castle.x * S, m.Castle.y * S, (c.castle.radius + c.castleSeedPad) * S, new Color32(214, 186, 130, 255));
|
||
var sp = b.SpawnPoint; float sr = (c.spawnJitter + 0.8f) * S;
|
||
r.Circle(ox + sp.x * S, sp.y * S, sr, new Color(1f, 1f, 1f, 0.16f));
|
||
r.Ring(ox + sp.x * S, sp.y * S, sr, S * 0.25f, new Color(1f, 1f, 1f, 0.55f));
|
||
if (m.River)
|
||
{
|
||
float ry = c.riverY * S, rh = c.riverHalf * S, hw = c.bridgeHalf * S;
|
||
r.RRect(-4, ry - rh, W + 4, ry + rh, 4, new Color32(70, 150, 230, 255)); // 강은 맵 밖 장식까지 이어짐
|
||
r.RRect(-4, ry - rh * 0.35f, W + 4, ry + rh * 0.35f, 3, new Color32(110, 190, 250, 255));
|
||
foreach (var bx in m.Bridges)
|
||
{
|
||
float x = ox + bx * S, step = (2 * rh + S * 0.6f) / 5f;
|
||
r.RRect(x - hw, ry - rh - S * 0.5f, x + hw, ry + rh + S * 0.5f, 4, new Color32(150, 100, 60, 255));
|
||
for (int k = 0; k < 5; k++) { float y0 = ry - rh - S * 0.3f + k * step; r.RRect(x - hw + S * 0.2f, y0, x + hw - S * 0.2f, y0 + S * 0.2f, 2, new Color32(190, 140, 90, 255)); }
|
||
}
|
||
}
|
||
foreach (var o in m.Obstacles) r.Ellipse(ox + o.pos.x * S + S * 0.2f, o.pos.y * S - S * 0.3f, o.r * S * 1.02f, o.r * S * 0.9f, new Color(0f, 0f, 0f, 0.22f)); // 그림자 먼저
|
||
foreach (var o in m.Obstacles)
|
||
{
|
||
float x = ox + o.pos.x * S, y = o.pos.y * S, rr = o.r * S;
|
||
if (o.kind == 0) // 바위
|
||
{
|
||
r.Circle(x, y, rr, new Color32(112, 110, 124, 255));
|
||
r.Circle(x - rr * 0.12f, y + rr * 0.12f, rr * 0.8f, new Color32(152, 150, 164, 255));
|
||
r.Circle(x - rr * 0.36f, y + rr * 0.36f, rr * 0.26f, new Color32(196, 194, 206, 255));
|
||
}
|
||
else // 언덕(흙 둔덕 + 풀)
|
||
{
|
||
r.Circle(x, y, rr, new Color32(128, 96, 60, 255));
|
||
r.Circle(x - rr * 0.05f, y + rr * 0.08f, rr * 0.86f, new Color32(178, 140, 90, 255));
|
||
r.Circle(x - rr * 0.25f, y + rr * 0.3f, rr * 0.45f, new Color32(208, 174, 120, 255));
|
||
r.Circle(x + rr * 0.32f, y + rr * 0.42f, rr * 0.18f, new Color32(96, 160, 80, 255));
|
||
}
|
||
}
|
||
return r.Sprite();
|
||
}
|
||
|
||
static readonly Dictionary<string, Sprite> bcache = new Dictionary<string, Sprite>();
|
||
|
||
static Sprite CastleSprite(bool enraged)
|
||
{
|
||
string key = "castle" + enraged;
|
||
if (bcache.TryGetValue(key, out var s)) return s;
|
||
var r = new Raster(200, 220);
|
||
Color stone = new Color32(150, 150, 168, 255), dark = new Color32(108, 108, 126, 255), roof = enraged ? new Color32(255, 80, 60, 255) : new Color32(220, 70, 80, 255);
|
||
r.RRect(20, 10, 180, 150, 14, dark); r.RRect(30, 20, 170, 140, 10, stone);
|
||
for (int i = 0; i < 5; i++) r.RRect(26 + i * 32, 140, 50 + i * 32, 168, 4, stone);
|
||
r.RRect(60, 20, 140, 90, 10, new Color32(90, 70, 60, 255));
|
||
r.Poly(new[] { new Vector2(40, 168), new Vector2(160, 168), new Vector2(100, 214) }, roof);
|
||
r.Star(100, 115, 20, Pal.Yellow);
|
||
s = r.Sprite(); bcache[key] = s; return s;
|
||
}
|
||
|
||
static readonly Color32[] UnitColor = { new Color32(80, 140, 255, 255), new Color32(90, 200, 110, 255), new Color32(170, 170, 190, 255), new Color32(170, 100, 240, 255), new Color32(200, 140, 90, 255), new Color32(110, 210, 140, 255) };
|
||
|
||
public static Sprite UnitSprite(int id)
|
||
{
|
||
string key = "u" + id;
|
||
if (bcache.TryGetValue(key, out var s)) return s;
|
||
s = UnitArt.ForUnit(id); // 도트 그림(Data/unit_art.json) 이 있으면 그것 · 없으면 아래 코드 그림
|
||
if (s != null) { bcache[key] = s; return s; }
|
||
var r = new Raster(96, 96);
|
||
Color c = UnitColor[(id - 1 + UnitColor.Length) % UnitColor.Length], dk = Color.Lerp(c, Pal.Ink, 0.4f), skin = new Color32(255, 220, 190, 255);
|
||
switch (id)
|
||
{
|
||
case 6: r.Poly(new[] { new Vector2(10, 60), new Vector2(48, 40), new Vector2(86, 60), new Vector2(48, 30) }, dk); r.Ellipse(48, 44, 22, 18, c); r.Circle(48, 66, 14, c); r.Circle(43, 70, 3, Pal.Ink); r.Circle(53, 70, 3, Pal.Ink); break;
|
||
case 5: r.Ellipse(48, 40, 34, 30, c); r.Circle(48, 74, 18, skin); r.Circle(16, 40, 12, skin); r.Circle(80, 40, 12, skin); r.Circle(42, 76, 3, Pal.Ink); r.Circle(54, 76, 3, Pal.Ink); break;
|
||
default:
|
||
r.Ellipse(48, 34, 22, 24, c); r.Circle(48, 66, 16, skin); r.Circle(43, 68, 3, Pal.Ink); r.Circle(53, 68, 3, Pal.Ink);
|
||
if (id == 1) { r.Line(new Vector2(70, 20), new Vector2(84, 70), 6, new Color32(200, 200, 215, 255)); r.Ellipse(48, 80, 18, 8, dk); }
|
||
if (id == 2) { r.Ring(78, 44, 18, 4, new Color32(150, 100, 60, 255)); r.Poly(new[] { new Vector2(30, 76), new Vector2(66, 76), new Vector2(48, 92) }, dk); }
|
||
if (id == 3) { r.RRect(10, 18, 34, 54, 8, dk); r.Star(22, 36, 8, Pal.Yellow); r.Ellipse(48, 80, 18, 8, new Color32(120, 120, 140, 255)); }
|
||
if (id == 4) { r.Poly(new[] { new Vector2(28, 76), new Vector2(68, 76), new Vector2(48, 96) }, dk); r.Circle(80, 52, 10, new Color32(255, 140, 255, 255)); r.Line(new Vector2(80, 44), new Vector2(80, 14), 4, new Color32(150, 100, 60, 255)); }
|
||
break;
|
||
}
|
||
s = r.Sprite(); bcache[key] = s; return s;
|
||
}
|
||
|
||
/// 성 소환수 — 붉은 몸 + 뿔 · 근접 = 몽둥이 · 원거리 = 활
|
||
static Sprite MinionSprite(bool ranged)
|
||
{
|
||
string key = ranged ? "m1" : "m0";
|
||
if (bcache.TryGetValue(key, out var s)) return s;
|
||
var r = new Raster(96, 96);
|
||
Color body = ranged ? new Color32(150, 50, 110, 255) : new Color32(178, 52, 52, 255), dk = Color.Lerp(body, Pal.Ink, 0.45f);
|
||
Color skin = ranged ? new Color32(236, 230, 214, 255) : new Color32(140, 196, 92, 255), eye = new Color32(220, 30, 30, 255);
|
||
r.Ellipse(48, 34, 22, 24, body);
|
||
r.Poly(new[] { new Vector2(34, 78), new Vector2(38, 95), new Vector2(45, 80) }, dk);
|
||
r.Poly(new[] { new Vector2(62, 78), new Vector2(58, 95), new Vector2(51, 80) }, dk);
|
||
r.Circle(48, 66, 16, skin); r.Circle(42, 68, 3.5f, eye); r.Circle(54, 68, 3.5f, eye);
|
||
if (ranged) { r.Ring(78, 44, 18, 4, new Color32(110, 70, 40, 255)); r.Line(new Vector2(78, 26), new Vector2(78, 62), 2, Color.white); }
|
||
else r.Line(new Vector2(70, 18), new Vector2(84, 56), 9, new Color32(120, 80, 50, 255));
|
||
s = r.Sprite(); bcache[key] = s; return s;
|
||
}
|
||
|
||
static Sprite FireballSprite()
|
||
{
|
||
if (bcache.TryGetValue("fire", out var s)) return s;
|
||
var r = new Raster(96, 96);
|
||
r.Circle(48, 48, 44, new Color(1f, 0.35f, 0.1f, 0.35f));
|
||
r.Circle(48, 48, 32, new Color32(255, 110, 40, 255));
|
||
r.Circle(44, 52, 20, new Color32(255, 190, 70, 255));
|
||
r.Circle(40, 56, 9, new Color32(255, 245, 200, 255));
|
||
s = r.Sprite(); bcache["fire"] = s; return s;
|
||
}
|
||
|
||
/// 레이저 띠(가로 = 길이) — 가운데 흰색 → 가장자리 분홍
|
||
static Sprite BeamSprite()
|
||
{
|
||
if (bcache.TryGetValue("beam", out var s)) return s;
|
||
var r = new Raster(16, 32);
|
||
for (int y = 0; y < 32; y++)
|
||
{
|
||
float d = Mathf.Abs(y + 0.5f - 16f) / 16f;
|
||
var c = Color.Lerp(Color.white, new Color(1f, 0.25f, 0.55f), Mathf.Clamp01(d * 1.6f)); c.a = Mathf.Clamp01(1.15f - d);
|
||
for (int x = 0; x < 16; x++) r.Px[y * 16 + x] = c;
|
||
}
|
||
s = r.Sprite(); bcache["beam"] = s; return s;
|
||
}
|
||
|
||
/// 번개(세로 · 아래 끝 = 맞는 곳)
|
||
static Sprite BoltSprite()
|
||
{
|
||
if (bcache.TryGetValue("bolt", out var s)) return s;
|
||
var r = new Raster(64, 256);
|
||
var pts = new[] { new Vector2(32, 0), new Vector2(18, 50), new Vector2(42, 96), new Vector2(20, 146), new Vector2(44, 196), new Vector2(30, 256) };
|
||
for (int i = 0; i < pts.Length - 1; i++) r.Line(pts[i], pts[i + 1], 14, new Color(1f, 0.95f, 0.4f, 0.45f));
|
||
for (int i = 0; i < pts.Length - 1; i++) r.Line(pts[i], pts[i + 1], 5, Color.white);
|
||
r.Circle(32, 8, 12, new Color(1f, 1f, 0.6f, 0.7f));
|
||
s = r.Sprite(); bcache["bolt"] = s; return s;
|
||
}
|
||
|
||
static Sprite StunSprite()
|
||
{
|
||
if (bcache.TryGetValue("stun", out var s)) return s;
|
||
var r = new Raster(96, 48);
|
||
r.Star(20, 24, 16, Pal.Yellow); r.Star(48, 30, 14, Pal.Yellow); r.Star(76, 24, 16, Pal.Yellow);
|
||
s = r.Sprite(); bcache["stun"] = s; return s;
|
||
}
|
||
|
||
static Sprite RingSprite()
|
||
{
|
||
if (bcache.TryGetValue("ring", out var s)) return s;
|
||
var r = new Raster(128, 128); r.Ring(64, 64, 56, 10, Color.white);
|
||
s = r.Sprite(); bcache["ring"] = s; return s;
|
||
}
|
||
|
||
/// 깃발(내 편 파랑) — 그림 왼쪽 아래 깃대 밑동 = 꽂은 곳
|
||
static Sprite FlagSprite()
|
||
{
|
||
if (bcache.TryGetValue("flag", out var s)) return s;
|
||
var r = new Raster(72, 104);
|
||
r.Ellipse(18, 8, 16, 6, new Color(0f, 0f, 0f, 0.3f));
|
||
r.Line(new Vector2(18, 8), new Vector2(18, 100), 6, new Color32(110, 76, 46, 255));
|
||
r.Poly(new[] { new Vector2(20, 98), new Vector2(68, 82), new Vector2(20, 64) }, new Color32(70, 130, 255, 255));
|
||
r.Poly(new[] { new Vector2(20, 92), new Vector2(50, 82), new Vector2(20, 72) }, new Color32(150, 200, 255, 255));
|
||
r.Circle(18, 100, 5, Pal.Yellow);
|
||
s = r.Sprite(); bcache["flag"] = s; return s;
|
||
}
|
||
|
||
/// 상대방 얼굴 = 방송 캐릭터 그림(의상 번호)의 머리 부분 · 분노 = 별 눈(프레임 3)
|
||
static Sprite RivalFace(int outfit, bool enraged)
|
||
{
|
||
string key = $"rival{outfit}{enraged}";
|
||
if (bcache.TryGetValue(key, out var s)) return s;
|
||
var tex = Art.Idol(outfit, enraged ? 3 : 0).texture;
|
||
s = Sprite.Create(tex, new Rect(40, 190, 220, 220), new Vector2(0.5f, 0.5f), 100f);
|
||
bcache[key] = s; return s;
|
||
}
|
||
|
||
/// 상대방 스킬 그림 — 성 투사체 5종 · 파이어 볼 · 라이트닝 · 소환수
|
||
static Sprite SkillIcon(string id)
|
||
{
|
||
string key = "sk_" + id;
|
||
if (bcache.TryGetValue(key, out var s)) return s;
|
||
var r = new Raster(96, 96);
|
||
switch (id)
|
||
{
|
||
case "fireball": s = FireballSprite(); bcache[key] = s; return s;
|
||
case "summonMelee": s = MinionSprite(false); bcache[key] = s; return s;
|
||
case "summonRanged": s = MinionSprite(true); bcache[key] = s; return s;
|
||
case "lightning":
|
||
{
|
||
var pts = new[] { new Vector2(60, 92), new Vector2(34, 52), new Vector2(56, 50), new Vector2(30, 4) };
|
||
for (int i = 0; i < pts.Length - 1; i++) r.Line(pts[i], pts[i + 1], 18, new Color(1f, 0.95f, 0.4f, 0.5f));
|
||
for (int i = 0; i < pts.Length - 1; i++) r.Line(pts[i], pts[i + 1], 8, new Color32(255, 240, 120, 255));
|
||
break;
|
||
}
|
||
case "splash": r.Ring(48, 48, 40, 6, new Color32(255, 160, 60, 200)); r.Circle(48, 48, 24, new Color32(255, 160, 60, 255)); r.Circle(42, 54, 8, new Color(1, 1, 1, 0.6f)); break;
|
||
case "multi":
|
||
for (int i = 0; i < 5; i++) { float a = (120f - 15f * i) * Mathf.Deg2Rad; r.Line(new Vector2(48, 10), new Vector2(48 + Mathf.Cos(a) * 78, 10 + Mathf.Sin(a) * 78), 7, new Color32(210, 255, 250, 255)); }
|
||
break;
|
||
case "slow": r.Circle(48, 48, 30, new Color32(140, 220, 255, 255)); for (int i = 0; i < 3; i++) { float a = (90f + 60f * i) * Mathf.Deg2Rad; var v = new Vector2(Mathf.Cos(a), Mathf.Sin(a)) * 40f; r.Line(new Vector2(48, 48) - v, new Vector2(48, 48) + v, 6, Color.white); } break;
|
||
case "laser": r.Line(new Vector2(8, 16), new Vector2(88, 80), 22, new Color(1f, 0.3f, 0.6f, 0.5f)); r.Line(new Vector2(8, 16), new Vector2(88, 80), 9, Color.white); break;
|
||
default: r.Circle(48, 48, 26, new Color32(255, 240, 160, 255)); r.Circle(40, 56, 9, new Color(1, 1, 1, 0.7f)); break; // normal
|
||
}
|
||
s = r.Sprite(); bcache[key] = s; return s;
|
||
}
|
||
|
||
static Sprite ManaDrop()
|
||
{
|
||
if (bcache.TryGetValue("drop", out var s)) return s;
|
||
var r = new Raster(96, 120);
|
||
r.Circle(48, 44, 38, new Color32(214, 60, 200, 255)); r.Poly(new[] { new Vector2(14, 56), new Vector2(82, 56), new Vector2(48, 116) }, new Color32(214, 60, 200, 255));
|
||
r.Circle(36, 56, 10, new Color(1, 1, 1, 0.5f));
|
||
s = r.Sprite(); bcache["drop"] = s; return s;
|
||
}
|
||
|
||
// ---------------- 화면 만들기 ----------------
|
||
// 클래시 로얄 비율 — 맵 = 화면 폭(좌우 장식 포함) · 아래 카드 칸(view.trayH) 바로 위 · 단계·시험(왼쪽 위)·상대방(오른쪽 위)은 맵 위에 겹쳐 놓는다
|
||
void BuildBattleScreen()
|
||
{
|
||
BattleRoot = UI.Rect("Battle", Root).Fill(); // 전투 = 화면 전체(아래 메뉴 숨김)
|
||
UI.Img(BattleRoot, "Bg", Art.Square, Skin.On ? Pal.Navy : (Color)new Color32(70, 66, 88, 255)).rectTransform.Fill();
|
||
if (Skin.Pattern != null) UI.Img(BattleRoot, "Pattern", Skin.Pattern, new Color(1f, 1f, 1f, 0.05f)).rectTransform.Fill();
|
||
var c = T.Battle; var v = c.view;
|
||
arenaImg = UI.Img(BattleRoot, "Arena", Art.Square, Color.white, true); // 그림은 NewBattleRound 에서 맵마다 넣는다 · 누르면 깃발
|
||
arena = arenaImg.rectTransform;
|
||
arena.Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, v.trayH + c.arenaH * v.tileY * 0.5f), new Vector2((c.arenaW + 2f * v.sideTiles) * v.tileX, c.arenaH * v.tileY));
|
||
var tap = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
|
||
tap.callback.AddListener(e => OnArenaTap((PointerEventData)e));
|
||
arenaImg.gameObject.AddComponent<EventTrigger>().triggers.Add(tap);
|
||
flagImg = UI.Img(arena, "Flag", FlagSprite(), Color.white);
|
||
flagImg.rectTransform.anchorMin = flagImg.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||
flagImg.rectTransform.pivot = new Vector2(18f / 72f, 8f / 104f); flagImg.rectTransform.sizeDelta = new Vector2(1.8f, 2.6f) * v.tileX; // 그림 72×104 비율
|
||
flagImg.gameObject.SetActive(false);
|
||
unitLayer = UI.Rect("Units", arena).Fill();
|
||
// 성 = 유닛과 같은 층(앞뒤 순서를 같이 정렬 — 성 뒤로 돌아간 유닛은 성에 가린다) · 그림 비율 그대로 칸 가로 기준
|
||
castleImg = UI.Img(unitLayer, "Castle", CastleSprite(false), Color.white);
|
||
castleImg.rectTransform.anchorMin = castleImg.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||
castleImg.rectTransform.sizeDelta = new Vector2(c.castle.radius * 2.4f, c.castle.radius * 2.6f) * v.tileX;
|
||
fxLayerB = UI.Rect("Fx", arena).Fill();
|
||
var hpBar = UI.Bar(arena, "CastleHp", new Color(0.12f, 0.12f, 0.2f, 0.9f), Pal.Red, out castleHpFill);
|
||
castleHpBar = hpBar.rectTransform;
|
||
castleHpBar.anchorMin = castleHpBar.anchorMax = new Vector2(0.5f, 0.5f);
|
||
castleHpBar.sizeDelta = new Vector2(220, 30);
|
||
castleHpText = UI.Txt(hpBar.transform, "T", "", 26, Color.white, TextAnchor.MiddleCenter, true); castleHpText.rectTransform.Fill();
|
||
// 오른쪽 위 = 상대방 얼굴 + 스킬 정보(클래시 로얄 남은 시간 칸 자리 · 2026-10-28 PD) — 맵 위에 겹치므로 작게
|
||
var rb = UI.Img(BattleRoot, "Rival", Skin.Round, new Color(0.17f, 0.19f, 0.32f, 0.92f));
|
||
rivalBox = rb.rectTransform;
|
||
rivalBox.Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-8, -8), new Vector2(RivalW, RivalTop), new Vector2(1, 1));
|
||
rivalFrame = UI.Img(rivalBox, "Frame", Skin.ProfileFrame, new Color32(255, 110, 150, 255));
|
||
rivalFrame.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(8, -8), new Vector2(100, 100), new Vector2(0, 1));
|
||
UI.Img(rivalFrame.transform, "In", Skin.ProfileInner, new Color32(255, 236, 244, 255)).rectTransform.Fill(7, 7, 7, 7);
|
||
rivalFace = UI.Img(rivalFrame.transform, "Face", null, Color.white); rivalFace.preserveAspect = true; rivalFace.rectTransform.Fill(9, 9, 9, 9);
|
||
UI.Txt(rivalBox, "Name", "상대방", 30, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(54, -58), new Vector2(-124, 44), new Vector2(0.5f, 0.5f));
|
||
// 왼쪽 위 = 단계(클래시 로얄 상대 이름 자리) · 아래 시험 버튼 — 성 지붕을 가리지 않게 폭 300
|
||
var stageBg = UI.Img(BattleRoot, "StageLabel", Skin.Label, Skin.On ? Color.white : new Color(0, 0, 0, 0));
|
||
stageBg.rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(10, -10), new Vector2(300, 76), new Vector2(0, 1));
|
||
stageText = UI.Txt(stageBg.transform, "Stage", "", 38, Color.white, TextAnchor.MiddleLeft, true);
|
||
stageText.rectTransform.Fill(30, 4, 16, 4);
|
||
stageText.supportRichText = true;
|
||
// 왼쪽 아래(내 진영 구석 — 유닛은 가운데에서 나옴) = 맵 정보 · 깃발 안내
|
||
var ib = UI.Img(BattleRoot, "InfoBg", Skin.RoundSoft, new Color(0.08f, 0.06f, 0.16f, 0.5f));
|
||
ib.rectTransform.Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(12, v.trayH + 10), new Vector2(360, 110), new Vector2(0, 0));
|
||
infoText = UI.Txt(ib.transform, "Info", "", 24, new Color32(236, 228, 255, 255), TextAnchor.UpperLeft, true);
|
||
infoText.rectTransform.Fill(14, 8, 10, 8);
|
||
infoText.supportRichText = true;
|
||
// 아래 카드 칸 + 마나 — 높이 view.trayH(클래시 로얄 ≈ 화면 폭 × 0.34) · 아래 116 = 마나 줄 · 위 = 카드
|
||
float cardH = v.trayH - 136f, cardY = v.trayH - 10f - cardH * 0.5f;
|
||
var panel = UI.Img(BattleRoot, "CardPanel", Skin.BottomBar, Skin.On ? Color.white : (Color)new Color32(120, 80, 50, 255));
|
||
panel.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), Vector2.zero, new Vector2(0, v.trayH), new Vector2(0.5f, 0));
|
||
UI.Img(panel.transform, "Inner", Skin.RoundSoft, Skin.On ? (Color)new Color32(54, 62, 98, 255) : (Color)new Color32(150, 104, 66, 255)).rectTransform.Fill(10, 10, 10, 10);
|
||
var upSp = Skin.Button(Pal.Orange);
|
||
var up = UI.Btn(panel.transform, "Upgrade", upSp ?? Art.Round, upSp != null ? Color.white : Pal.Orange, () => OpenBattleUpgrade());
|
||
up.GetComponent<RectTransform>().Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(84, cardY), new Vector2(130, Mathf.Min(150f, cardH)));
|
||
UI.Icon(up.transform, "I", Skin.Icon("up"), Color.white).rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, -46), new Vector2(70, 70));
|
||
UI.Txt(up.transform, "T", "강화", 32, Color.white, TextAnchor.MiddleCenter, true).rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 34), new Vector2(0, 44));
|
||
var row = UI.Rect("Cards", panel.transform).Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(80, cardY), new Vector2(-200, cardH), new Vector2(0.5f, 0.5f));
|
||
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.spacing = 12; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = true;
|
||
for (int i = 0; i < c.slotMax; i++)
|
||
{
|
||
int slot = i;
|
||
var b = UI.Btn(row, "Card" + i, Skin.On ? Skin.CardOn : Art.Round, Skin.On ? Color.white : (Color)new Color32(90, 160, 230, 255), () => OnCard(slot));
|
||
var face = UI.Img(b.transform, "Face", null, Color.white); face.preserveAspect = true; face.rectTransform.Fill(12, 50, 12, 14);
|
||
var gem = UI.Icon(b.transform, "Gem", Skin.Gem ?? ManaDrop(), Color.white); gem.rectTransform.Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(0, 16), new Vector2(78, 72));
|
||
var cost = UI.Txt(gem.transform, "C", "", 38, Color.white, TextAnchor.MiddleCenter, true); cost.rectTransform.Fill(0, 4, 0, 0);
|
||
var lk = UI.Icon(b.transform, "Lock", Skin.Icon("lock"), Color.white); lk.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0, 20), new Vector2(80, 80));
|
||
var lt = UI.Txt(b.transform, "LT", "", 24, Color.white, TextAnchor.MiddleCenter, true); lt.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 26), new Vector2(0, 40));
|
||
cards.Add((b, face, cost, lk, lt));
|
||
}
|
||
var drop = UI.Icon(panel.transform, "Drop", Skin.ManaGem ?? ManaDrop(), Color.white);
|
||
drop.rectTransform.Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(84, 70), new Vector2(110, 100));
|
||
manaText = UI.Txt(drop.transform, "N", "5", 50, Color.white, TextAnchor.MiddleCenter, true); manaText.rectTransform.Fill(0, 4, 0, 0);
|
||
var mbar = UI.Img(panel.transform, "ManaBar", Skin.BarBgThick, Skin.On ? (Color)new Color32(40, 36, 66, 255) : new Color(0.18f, 0.1f, 0.2f, 0.9f));
|
||
mbar.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(90, 70), new Vector2(-240, 56), new Vector2(0.5f, 0.5f));
|
||
manaFill = UI.Img(mbar.transform, "Fill", Skin.BarFill, new Color32(220, 80, 210, 255));
|
||
manaFill.type = Image.Type.Filled; manaFill.fillMethod = Image.FillMethod.Horizontal; manaFill.preserveAspect = false; manaFill.rectTransform.Fill(7, 7, 7, 7);
|
||
manaMaxText = UI.Txt(panel.transform, "Max", "", 28, Color.white, TextAnchor.MiddleLeft, true);
|
||
manaMaxText.rectTransform.Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(150, 26), new Vector2(300, 40), new Vector2(0, 0.5f));
|
||
// 결과 덮개(승리·패배) — 왼쪽 「스테이지 선택」 · 오른쪽 = 다음 스테이지 / 다시 도전 (시작은 스테이지 선택 화면의 「시작」)
|
||
var ov = UI.Img(BattleRoot, "Overlay", Art.Square, new Color(0.05f, 0.03f, 0.1f, 0.55f), true);
|
||
overlay = ov.rectTransform.Fill(0, v.trayH, 0, 0);
|
||
var card = UI.Img(overlay, "Card", Skin.PopupPlain, Pal.Panel);
|
||
card.rectTransform.Place(new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(840, 800));
|
||
overlayRibbon = UI.Img(card.transform, "Ribbon", Skin.Ribbon("Blue"), Skin.On ? Color.white : new Color(0, 0, 0, 0));
|
||
overlayRibbon.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, 50), new Vector2(560, 230));
|
||
overlayTitle = UI.Txt(overlayRibbon.transform, "Title", "", 64, Skin.On ? Color.white : Pal.Purple, TextAnchor.MiddleCenter, true); overlayTitle.rectTransform.Fill(120, 70, 120, 50);
|
||
overlayInfo = UI.Txt(card.transform, "Info", "", 34, Pal.Ink); overlayInfo.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, -350), new Vector2(-60, 400));
|
||
var ex = UI.TextBtn(card.transform, "Exit", "스테이지 선택", Pal.Gray, 40, () => OpenStageSelect(B.State == BattleState.Won ? G.StageFocus : B.Stage), out _);
|
||
ex.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(-195, 90), new Vector2(370, 150));
|
||
var ob = UI.TextBtn(card.transform, "Go", "", Pal.Green, 44, () => OnOverlayButton(), out overlayBtnLabel);
|
||
ob.GetComponent<RectTransform>().Place(new Vector2(0.5f, 0), new Vector2(0.5f, 0), new Vector2(195, 90), new Vector2(370, 150));
|
||
// 시험 버튼(덮개 위 — 결과 화면에서도 누를 수 있게)
|
||
var tb = UI.TextBtn(BattleRoot, "TestBtn", "시험", Pal.Purple, 30, () => OpenBattleTest(), out _);
|
||
tb.GetComponent<RectTransform>().Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(14, -94), new Vector2(130, 72), new Vector2(0, 1));
|
||
BattleRoot.gameObject.SetActive(false);
|
||
BuildStageScreen();
|
||
BuildUnitScreen();
|
||
}
|
||
|
||
/// 고른 스테이지(stageSel)로 새 전투 준비 + 전투 화면 띄움 — 시작(B.Start)은 부르는 쪽
|
||
public void NewBattleRound(int seed)
|
||
{
|
||
foreach (var v in unitViews.Values) Kill(v.rt.gameObject);
|
||
unitViews.Clear();
|
||
foreach (var v in minionViews.Values) Kill(v.rt.gameObject);
|
||
minionViews.Clear();
|
||
B = G.NewBattle(stageSel, seed, BattleTest);
|
||
if (Tab == 1) { BattleRoot.gameObject.SetActive(true); StageRoot.gameObject.SetActive(false); UpdateTabBar(); }
|
||
rewardGiven = false; lastBattleReward = BigInteger.Zero;
|
||
var old = arenaImg.sprite;
|
||
arenaImg.sprite = ArenaSprite(B);
|
||
if (old != null && old != Art.Square)
|
||
{
|
||
var tex = old.texture;
|
||
if (Application.isPlaying) { Destroy(old); Destroy(tex); } else { DestroyImmediate(old); DestroyImmediate(tex); }
|
||
}
|
||
var cp = castleImg.rectTransform.anchoredPosition = TileToLocal(B.CastlePos);
|
||
// 성 체력 막대 = 지붕 위쪽 · 맵 맨 위를 넘지 않게
|
||
castleHpBar.anchoredPosition = new Vector2(cp.x, Mathf.Min(cp.y + castleImg.rectTransform.sizeDelta.y * 0.5f - 6f, arena.sizeDelta.y * 0.5f - castleHpBar.sizeDelta.y * 0.5f - 6f));
|
||
flagPop = 0f;
|
||
BuildRivalRows();
|
||
RefreshBattle();
|
||
}
|
||
|
||
// ---------------- 상대방 스킬 줄: 그림 · 이름 · 수치(성 공격력 × %) · 재사용 막대 ----------------
|
||
void BuildRivalRows()
|
||
{
|
||
foreach (var r in rivalRows) Kill(r.rt.gameObject);
|
||
rivalRows.Clear();
|
||
foreach (var w in B.CastleShots) AddRivalRow(w.def.id, w.def.name, RivalInfo(w.def), w, null);
|
||
foreach (var w in B.CastleSkills) AddRivalRow(w.def.id, w.def.name, RivalInfo(w.def), w, null);
|
||
foreach (var s in B.Summoners) AddRivalRow(s.def.id, s.def.name, "", null, s);
|
||
rivalBox.sizeDelta = new Vector2(rivalBox.sizeDelta.x, RivalTop + rivalRows.Count * RivalRowH + 8f);
|
||
}
|
||
|
||
void AddRivalRow(string id, string name, string info, BWeapon w, BSummoner s)
|
||
{
|
||
float y = -RivalTop - rivalRows.Count * RivalRowH;
|
||
var bg = UI.Img(rivalBox, "Skill_" + id, Skin.RoundSoft, new Color(1f, 1f, 1f, 0.08f));
|
||
// 한 줄 = 왼쪽 그림 · 오른쪽 이름 + 수치 2줄 · 맨 아래 재사용 막대
|
||
var rt = bg.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, y), new Vector2(-12, RivalRowH - 6f), new Vector2(0.5f, 1));
|
||
UI.Img(rt, "Icon", SkillIcon(id), Color.white).rectTransform.Place(new Vector2(0, 1), new Vector2(0, 1), new Vector2(4, -4), new Vector2(44, 44), new Vector2(0, 1));
|
||
UI.Txt(rt, "Name", name, 22, Pal.Yellow, TextAnchor.MiddleLeft, true).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(26, -2), new Vector2(-58, 28), new Vector2(0.5f, 1));
|
||
var tx = UI.Txt(rt, "Info", info, 18, new Color32(236, 228, 255, 255), TextAnchor.UpperLeft);
|
||
tx.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(26, -30), new Vector2(-58, 42), new Vector2(0.5f, 1));
|
||
var bar = UI.Bar(rt, "Cd", new Color(0.1f, 0.1f, 0.18f, 0.9f), new Color32(255, 200, 90, 255), out var fill, true);
|
||
bar.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 3), new Vector2(-12, 7), new Vector2(0.5f, 0));
|
||
rivalRows.Add(new RivalRow { rt = rt, bg = bg, fill = fill, info = tx, w = w, s = s, lastCd = w != null ? w.cd : s.cd });
|
||
}
|
||
|
||
/// 피해 = 성 공격력 × dmgPct % (battle.json) · 사거리 = 시험 배율 반영
|
||
string RivalInfo(BAttackDef d) => AttackInfo(d, B.Atk, B.Range(d));
|
||
|
||
/// 성 공격 한 가지 설명 2줄 — 피해(성 공격력 × %) · 덧붙는 효과 · 사거리(칸) (스테이지 정보 창과 같이 씀)
|
||
static string AttackInfo(BAttackDef d, float atk, float range)
|
||
{
|
||
int dmg = Mathf.RoundToInt(atk * d.dmgPct / 100f), sp = Mathf.RoundToInt(atk * d.splashPct / 100f);
|
||
string a = d.id == "multi" ? $"피해 {dmg} × {d.count}발" : d.id == "laser" ? $"줄 위 전원 {dmg}" : $"피해 {dmg}";
|
||
string b = d.splashRadius > 0f ? $"주변 {sp}" : d.slowSeconds > 0f ? $"감속 {d.slowPct:0}%" : d.stunSeconds > 0f ? $"기절 {d.stunSeconds:0.#}초" : "";
|
||
return a + "\n" + (b.Length > 0 ? b + " · " : "") + $"{range:0.#}칸";
|
||
}
|
||
|
||
void RefreshRival()
|
||
{
|
||
rivalFace.sprite = RivalFace(B.RivalOutfit, B.Enraged);
|
||
rivalFace.color = B.CastleHitFlash > 0 ? new Color(1f, 0.75f, 0.75f) : Color.white;
|
||
rivalFrame.color = B.Enraged ? (Color)new Color32(255, 70, 60, 255) : (Color)new Color32(255, 110, 150, 255);
|
||
foreach (var r in rivalRows)
|
||
{
|
||
float cd = r.w != null ? r.w.cd : r.s.cd, full = r.w != null ? r.w.def.interval * B.IntervalMul : r.s.def.interval;
|
||
if (cd > r.lastCd + 0.05f) r.flash = 0.35f; // 방금 발동 (TickBattle 에서 줄어듦)
|
||
r.lastCd = cd;
|
||
r.fill.fillAmount = Mathf.Clamp01(1f - cd / Mathf.Max(0.01f, full));
|
||
r.bg.color = r.flash > 0f ? new Color(1f, 0.85f, 0.4f, 0.55f) : new Color(1f, 1f, 1f, 0.08f);
|
||
if (r.s != null)
|
||
{
|
||
int n = 0; foreach (var m in B.Minions) if (m.def == r.s.def) n++;
|
||
r.info.text = $"체력 {Mathf.RoundToInt(r.s.def.hp * B.SummonMul)}\n공격 {Mathf.RoundToInt(r.s.def.damage * B.SummonMul)} · {n}/{r.s.def.max}";
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------- 깃발: 맵을 누른 곳 → 내 유닛이 먼저 깃발에 닿은 뒤 성으로 ----------------
|
||
void OnArenaTap(PointerEventData e)
|
||
{
|
||
if (B == null || B.State != BattleState.Fighting) return;
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(arena, e.position, e.pressEventCamera, out var lp))
|
||
PlaceFlag(new Vector2(lp.x / V.tileX + B.C.arenaW * 0.5f, lp.y / V.tileY + B.C.arenaH * 0.5f));
|
||
}
|
||
|
||
public bool PlaceFlag(Vector2 tile)
|
||
{
|
||
if (B == null || !B.SetFlag(tile)) return false;
|
||
flagPop = 0.25f;
|
||
RefreshBattle();
|
||
return true;
|
||
}
|
||
|
||
BigInteger lastBattleReward;
|
||
|
||
/// 결과 오른쪽 버튼 — 이겼고 다음 스테이지가 열렸으면 다음 스테이지 · 아니면 같은 스테이지 다시
|
||
void OnOverlayButton()
|
||
{
|
||
if (B == null) return;
|
||
BeginBattle(NextOpen(B) ? B.Stage + 1 : B.Stage);
|
||
}
|
||
|
||
bool NextOpen(Battle b) => b.State == BattleState.Won && b.Stage < G.StageCount && G.StageOpen(b.Stage + 1);
|
||
|
||
void OnCard(int slot)
|
||
{
|
||
if (slot >= G.BattleSlots) { OpenBattleUpgrade(); return; }
|
||
DeployCard(slot);
|
||
}
|
||
|
||
/// 카드 = 즉시 소환(아래 가운데) — 위치 선택 없음
|
||
public bool DeployCard(int slot)
|
||
{
|
||
var d = G.BattleUnit(slot);
|
||
if (d == null || B == null || B.State != BattleState.Fighting) return false;
|
||
if (B.Mana < d.cost) { Toast("마나가 부족합니다"); return false; }
|
||
bool ok = B.Deploy(d);
|
||
RefreshBattle();
|
||
return ok;
|
||
}
|
||
|
||
public void OpenBattleUpgrade()
|
||
{
|
||
var p = NewPopup("BattleUpgrade", "전투 강화", 1000, Pal.Orange);
|
||
var t1 = UI.Txt(p.body, "Slots", "", 40, Pal.Ink, TextAnchor.MiddleLeft); t1.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(20, -90), new Vector2(-400, 120), new Vector2(0, 0.5f));
|
||
var b1 = UI.TextBtn(p.body, "UnlockSlot", "", Pal.Green, 34, () => { var r = G.UnlockBattleSlot(); if (r != BuyResult.Ok) Toast(BuyMsg(r)); RefreshBattle(); RefreshAll(); }, out var l1);
|
||
b1.GetComponent<RectTransform>().Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-190, -90), new Vector2(340, 140));
|
||
var t2 = UI.Txt(p.body, "Mana", "", 40, Pal.Ink, TextAnchor.MiddleLeft); t2.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(20, -290), new Vector2(-400, 120), new Vector2(0, 0.5f));
|
||
var b2 = UI.TextBtn(p.body, "UpMana", "", Pal.Purple, 34, () => { var r = G.UpgradeBattleMana(); if (r != BuyResult.Ok) Toast(BuyMsg(r)); RefreshBattle(); RefreshAll(); }, out var l2);
|
||
b2.GetComponent<RectTransform>().Place(new Vector2(1, 1), new Vector2(1, 1), new Vector2(-190, -290), new Vector2(340, 140));
|
||
var note = UI.Txt(p.body, "Note", "비용·능력치 = 설계값(확정 전) · 강화는 다음 전투부터 적용", 28, Pal.Gray); note.rectTransform.Place(new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 60), new Vector2(-40, 60));
|
||
p.refresh = () =>
|
||
{
|
||
var nu = G.BattleUnit(G.BattleSlots);
|
||
t1.text = $"덱 칸 {G.BattleSlots} / {T.Battle.slotMax}" + (G.BattleSlots < T.Battle.slotMax ? $"\n다음 칸: {(nu != null ? $"{nu.name} ({nu.cost} 마나)" : "빈 칸")}" : "");
|
||
bool mx1 = G.BattleSlots >= T.Battle.slotMax; l1.text = mx1 ? "MAX" : "칸 열기\n" + Fmt.Num(G.SlotUnlockPrice()); b1.interactable = !mx1 && G.SlotUnlockPrice() <= G.Coin;
|
||
t2.text = $"최대 마나 {G.BattleMaxMana} / {T.Battle.manaMaxCap}";
|
||
bool mx2 = G.BattleMaxMana >= T.Battle.manaMaxCap; l2.text = mx2 ? "MAX" : "최대 +1\n" + Fmt.Num(G.ManaUpgradePrice()); b2.interactable = !mx2 && G.ManaUpgradePrice() <= G.Coin;
|
||
};
|
||
p.refresh();
|
||
}
|
||
|
||
// ---------------- 시험 설정: 맵 · 바위·언덕 · 성 투사체 · 성 기믹 · 사거리 배율 → 다음 전투 ----------------
|
||
BattleSetup StageDefaultSetup()
|
||
{
|
||
var lo = T.Battle.Loadout(stageSel);
|
||
return new BattleSetup { shots = new List<string>(lo.shots ?? new string[0]), gimmicks = new List<string>(lo.gimmicks ?? new string[0]) };
|
||
}
|
||
|
||
public void OpenBattleTest()
|
||
{
|
||
var c = T.Battle;
|
||
var s = BattleTest != null ? BattleTest.Clone() : StageDefaultSetup();
|
||
var def = StageDefaultSetup();
|
||
if (s.shots == null) s.shots = def.shots;
|
||
if (s.gimmicks == null) s.gimmicks = def.gimmicks;
|
||
var p = NewPopup("BattleTest", "전투 시험 설정", 1600, Pal.Purple);
|
||
var refreshers = new List<System.Action>();
|
||
float y = -10f;
|
||
var mapNames = new List<string> { "무작위" }; mapNames.AddRange(c.mapNames);
|
||
y = TestRow(p, refreshers, y, "맵", mapNames, k => s.mapType == k - 1, k => s.mapType = k - 1);
|
||
y = TestRow(p, refreshers, y, "바위·언덕", new List<string> { "무작위", "없음", "있음" }, k => s.obstacles == k - 1, k => s.obstacles = k - 1);
|
||
var shotIds = c.shots.ConvertAll(d => d.id);
|
||
y = TestRow(p, refreshers, y, "성 투사체 (여러 개 선택)", c.shots.ConvertAll(d => d.name), k => s.shots.Contains(shotIds[k]), k => Toggle(s.shots, shotIds[k]));
|
||
var gIds = c.skills.ConvertAll(d => d.id); gIds.AddRange(c.summons.ConvertAll(d => d.id));
|
||
var gNames = c.skills.ConvertAll(d => d.name); gNames.AddRange(c.summons.ConvertAll(d => d.name));
|
||
y = TestRow(p, refreshers, y, "성 기믹 (여러 개 선택)", gNames, k => s.gimmicks.Contains(gIds[k]), k => Toggle(s.gimmicks, gIds[k]));
|
||
var muls = new List<float>(c.testRangeMuls);
|
||
y = TestRow(p, refreshers, y, "성 사거리 배율", muls.ConvertAll(m => "×" + m.ToString("0.##")), k => Mathf.Abs(s.rangeMul - muls[k]) < 1e-4f, k => s.rangeMul = muls[k]);
|
||
var note = UI.Txt(p.body, "Note", $"적용 = 이 설정으로 스테이지 {stageSel} 바로 시작(잠긴 스테이지도 가능 · 시험 전투는 클리어 기록 안 함) · 수치 = battle.json (설계값 · 확정 전)", 28, Pal.Gray);
|
||
note.rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, y - 30), new Vector2(-20, 90), new Vector2(0.5f, 1));
|
||
// 단계 기본으로 = 시험 해제(싸우는 중이면 기본 설정으로 다시 시작 · 스테이지 선택 화면이면 그대로)
|
||
var reset = UI.TextBtn(p.body, "Reset", "단계 기본으로", Pal.Gray, 38, () => { BattleTest = null; CloseAllPopups(); if (B != null && B.State == BattleState.Fighting) BeginBattle(stageSel); else ShowStage(stageSel); }, out _);
|
||
reset.GetComponent<RectTransform>().Place(new Vector2(0, 0), new Vector2(0, 0), new Vector2(220, 80), new Vector2(400, 130));
|
||
var apply = UI.TextBtn(p.body, "Apply", "적용 → 새 전투", Pal.Green, 38, () => { BattleTest = s; CloseAllPopups(); BeginBattle(stageSel); }, out _);
|
||
apply.GetComponent<RectTransform>().Place(new Vector2(1, 0), new Vector2(1, 0), new Vector2(-220, 80), new Vector2(400, 130));
|
||
p.refresh = () => { foreach (var r in refreshers) r(); };
|
||
p.refresh();
|
||
}
|
||
|
||
static void Toggle(List<string> l, string id) { if (!l.Remove(id)) l.Add(id); }
|
||
|
||
float TestRow(Popup p, List<System.Action> refreshers, float y, string label, List<string> names, System.Func<int, bool> isOn, System.Action<int> click)
|
||
{
|
||
UI.Txt(p.body, "L" + label, label, 34, Pal.Ink, TextAnchor.MiddleLeft).rectTransform.Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(10, y), new Vector2(-20, 52), new Vector2(0.5f, 1));
|
||
y -= 58f;
|
||
var row = UI.Rect("R" + label, p.body).Place(new Vector2(0, 1), new Vector2(1, 1), new Vector2(0, y), new Vector2(0, 104), new Vector2(0.5f, 1));
|
||
var hl = row.gameObject.AddComponent<HorizontalLayoutGroup>();
|
||
hl.spacing = 10; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = true; hl.childForceExpandHeight = true;
|
||
for (int i = 0; i < names.Count; i++)
|
||
{
|
||
int k = i;
|
||
var b = UI.TextBtn(row, "B" + i, names[i], Pal.Gray, 30, () => { click(k); p.refresh?.Invoke(); }, out _);
|
||
refreshers.Add(() => UI.SetBtnColor(b, isOn(k) ? Pal.Pink : Pal.Gray));
|
||
}
|
||
return y - 124f;
|
||
}
|
||
|
||
// ---------------- 진행 · 갱신 ----------------
|
||
void TickBattle(float dt)
|
||
{
|
||
if (B == null || Tab != 1) return;
|
||
B.Tick(dt);
|
||
flagPop = Mathf.Max(0f, flagPop - dt);
|
||
foreach (var r in rivalRows) r.flash = Mathf.Max(0f, r.flash - dt);
|
||
if (B.State == BattleState.Won && !rewardGiven) { rewardGiven = true; lastBattleReward = G.WinBattle(B); }
|
||
RefreshBattle();
|
||
}
|
||
|
||
static string JoinNames(List<string> l, string sep) => l.Count > 0 ? string.Join(sep, l) : "없음";
|
||
|
||
void RefreshBattle()
|
||
{
|
||
if (B == null || castleImg == null) return;
|
||
stageText.text = $"스테이지 {B.Stage}" + (B.IsTest ? " <color=#9CF0FF>시험</color>" : "") + (B.LastMinute && B.State == BattleState.Fighting ? " <color=#FF9CF5>마나 x2</color>" : "");
|
||
castleHpFill.fillAmount = B.CastleMaxHp > 0 ? B.CastleHp / B.CastleMaxHp : 0;
|
||
castleHpFill.color = B.CastleHealFlash > 0f ? (Color)new Color32(90, 230, 120, 255) : Pal.Red;
|
||
castleHpText.text = Mathf.CeilToInt(B.CastleHp).ToString();
|
||
castleImg.sprite = CastleSprite(B.Enraged);
|
||
castleImg.color = B.CastleHitFlash > 0 ? new Color(1f, 0.75f, 0.75f) : Color.white;
|
||
manaText.text = Mathf.FloorToInt(B.Mana).ToString();
|
||
manaFill.fillAmount = B.MaxMana > 0 ? B.Mana / B.MaxMana : 0;
|
||
manaMaxText.text = $"최대: {Mathf.RoundToInt(B.MaxMana)}";
|
||
infoText.text = $"<color=#FFE27A>맵</color> {B.MapName} · 바위·언덕 {B.Map.Obstacles.Count}\n<color=#FFE27A>깃발</color> 맵을 누르면 유닛이 깃발을 거쳐 성으로";
|
||
RefreshRival();
|
||
flagImg.gameObject.SetActive(B.HasFlag && B.State == BattleState.Fighting);
|
||
if (B.HasFlag)
|
||
{
|
||
flagImg.rectTransform.anchoredPosition = TileToLocal(B.FlagPos);
|
||
flagImg.rectTransform.localScale = Vector3.one * (1f + flagPop * 2f);
|
||
flagImg.rectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Sin(Time.unscaledTime * 3f) * 3f);
|
||
}
|
||
for (int i = 0; i < cards.Count; i++)
|
||
{
|
||
var (b, face, cost, lockImg, lockText) = cards[i];
|
||
bool open = i < G.BattleSlots; var d = G.BattleUnit(i);
|
||
face.gameObject.SetActive(open && d != null); cost.transform.parent.gameObject.SetActive(open && d != null);
|
||
lockImg.gameObject.SetActive(!open); lockText.gameObject.SetActive(!open || d == null);
|
||
if (open && d != null) { face.sprite = UnitSprite(d.id); cost.text = d.cost.ToString(); }
|
||
lockText.text = !open ? (i == G.BattleSlots ? "강화" : "") : d == null ? "빈 칸" : "";
|
||
bool afford = open && d != null && B.Mana >= d.cost;
|
||
var ci = b.GetComponent<Image>();
|
||
if (Skin.On) { ci.sprite = open ? Skin.CardOn : Skin.CardOff; ci.color = !open ? new Color(0.8f, 0.8f, 0.86f) : (afford ? Color.white : new Color(0.55f, 0.6f, 0.72f)); }
|
||
else ci.color = !open ? new Color32(70, 60, 80, 255) : (afford ? new Color32(90, 160, 230, 255) : new Color32(110, 120, 140, 255));
|
||
}
|
||
// 내 유닛 (기절 = 별 · 감속 = 파란 빛)
|
||
var alive = new HashSet<BUnit>(B.Units);
|
||
var dead = new List<BUnit>();
|
||
foreach (var kv in unitViews) if (!alive.Contains(kv.Key)) dead.Add(kv.Key);
|
||
foreach (var u in dead) { Kill(unitViews[u].rt.gameObject); unitViews.Remove(u); }
|
||
foreach (var u in B.Units)
|
||
{
|
||
if (!unitViews.TryGetValue(u, out var v))
|
||
{
|
||
var im = UI.Img(unitLayer, "U" + u.uid, UnitSprite(u.def.id), Color.white);
|
||
float sz = u.def.radius * V.unitArt * V.tileX; // 그림 한 변 = 반지름 × unitArt 칸(클래시 로얄 병사 키 ≈ 1.5칸)
|
||
im.rectTransform.anchorMin = im.rectTransform.anchorMax = new Vector2(0.5f, 0.5f); im.rectTransform.pivot = new Vector2(0.5f, 0f); im.rectTransform.sizeDelta = new Vector2(sz, sz); // 그림 발밑 = 유닛 자리
|
||
var hb = UI.Bar(im.transform, "Hp", new Color(0.1f, 0.1f, 0.18f, 0.85f), new Color32(90, 200, 255, 255), out var hf, true);
|
||
hb.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, 6), new Vector2(sz * 0.6f, 12));
|
||
var st = UI.Img(im.transform, "Stun", StunSprite(), Color.white);
|
||
st.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, 30), new Vector2(sz * 0.7f, sz * 0.35f));
|
||
v = (im.rectTransform, hf, st); unitViews[u] = v;
|
||
}
|
||
v.rt.anchoredPosition = TileToLocal(u.pos) + (u.def.flying ? new Vector2(0, V.flyLift * V.tileY) : Vector2.zero);
|
||
v.hp.fillAmount = Mathf.Clamp01(u.hp / Mathf.Max(1f, u.def.hp));
|
||
v.rt.GetComponent<Image>().color = u.hitFlash > 0 ? new Color(1f, 0.6f, 0.6f) : (u.slow > 0f ? new Color(0.55f, 0.78f, 1f) : Color.white);
|
||
v.rt.localScale = Vector3.one * (u.attacking ? 1f + 0.08f * Mathf.Sin(Time.unscaledTime * 18f) : 1f);
|
||
v.stun.gameObject.SetActive(u.stun > 0f);
|
||
if (u.stun > 0f) v.stun.rectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Sin(Time.unscaledTime * 6f) * 15f);
|
||
}
|
||
// 성 소환수
|
||
var aliveM = new HashSet<BMinion>(B.Minions);
|
||
var deadM = new List<BMinion>();
|
||
foreach (var kv in minionViews) if (!aliveM.Contains(kv.Key)) deadM.Add(kv.Key);
|
||
foreach (var m in deadM) { Kill(minionViews[m].rt.gameObject); minionViews.Remove(m); }
|
||
foreach (var m in B.Minions)
|
||
{
|
||
if (!minionViews.TryGetValue(m, out var v))
|
||
{
|
||
var im = UI.Img(unitLayer, "M" + m.uid, MinionSprite(m.def.ranged), Color.white);
|
||
float sz = m.def.radius * V.unitArt * V.tileX;
|
||
im.rectTransform.anchorMin = im.rectTransform.anchorMax = new Vector2(0.5f, 0.5f); im.rectTransform.pivot = new Vector2(0.5f, 0f); im.rectTransform.sizeDelta = new Vector2(sz, sz);
|
||
var hb = UI.Bar(im.transform, "Hp", new Color(0.1f, 0.1f, 0.18f, 0.85f), Pal.Red, out var hf, true);
|
||
hb.rectTransform.Place(new Vector2(0.5f, 1), new Vector2(0.5f, 1), new Vector2(0, 6), new Vector2(sz * 0.6f, 12));
|
||
v = (im.rectTransform, hf); minionViews[m] = v;
|
||
}
|
||
v.rt.anchoredPosition = TileToLocal(m.pos);
|
||
v.hp.fillAmount = Mathf.Clamp01(m.hp / Mathf.Max(1f, m.maxHp));
|
||
v.rt.GetComponent<Image>().color = m.hitFlash > 0 ? new Color(1f, 0.6f, 0.6f) : Color.white;
|
||
v.rt.localScale = Vector3.one * (m.attacking ? 1f + 0.08f * Mathf.Sin(Time.unscaledTime * 18f) : 1f);
|
||
}
|
||
// 앞뒤 순서 — 아래(가까운) 것이 위(먼) 것을 가린다 · 성은 가운데 줄 기준 · 나는 유닛은 맨 앞
|
||
depth.Clear();
|
||
depth.Add((castleImg.transform, B.CastlePos.y));
|
||
foreach (var kv in unitViews) depth.Add((kv.Value.rt, kv.Key.def.flying ? kv.Key.pos.y - 1000f : kv.Key.pos.y));
|
||
foreach (var kv in minionViews) depth.Add((kv.Value.rt, kv.Key.pos.y));
|
||
depth.Sort((a, b) => b.key.CompareTo(a.key));
|
||
for (int i = 0; i < depth.Count; i++) if (depth[i].t.GetSiblingIndex() != i) depth[i].t.SetSiblingIndex(i);
|
||
// 투사체 — 크기 = 칸 가로 기준 · 유닛 그림이 발밑에서 위로 서 있으므로 몸통 높이(0.9칸)로 띄워 날린다(파이어 볼 = 땅에 떨어짐)
|
||
while (shotViews.Count < B.Shots.Count) { var s = UI.Img(fxLayerB, "Shot", Art.Circle, Color.white); s.rectTransform.anchorMin = s.rectTransform.anchorMax = new Vector2(0.5f, 0.5f); shotViews.Add(s); }
|
||
float tw = V.tileX;
|
||
for (int i = 0; i < shotViews.Count; i++)
|
||
{
|
||
bool on = i < B.Shots.Count; shotViews[i].gameObject.SetActive(on); if (!on) continue;
|
||
var sh = B.Shots[i]; var im = shotViews[i]; var rt = im.rectTransform;
|
||
rt.anchoredPosition = TileToLocal(sh.pos) + new Vector2(0f, sh.type == "fireball" ? 0f : 0.9f * V.tileY); rt.localEulerAngles = Vector3.zero; im.sprite = Art.Circle;
|
||
switch (sh.type)
|
||
{
|
||
case "fireball": im.sprite = FireballSprite(); rt.sizeDelta = Vector2.one * 1.6f * tw; im.color = Color.white; break;
|
||
case "splash": rt.sizeDelta = Vector2.one * 0.7f * tw; im.color = new Color32(255, 160, 60, 255); break;
|
||
case "slow": rt.sizeDelta = Vector2.one * 0.6f * tw; im.color = new Color32(140, 220, 255, 255); break;
|
||
case "multi": { var dp = TileVec(sh.dir); rt.sizeDelta = new Vector2(0.85f, 0.35f) * tw; im.color = new Color32(210, 255, 250, 255); rt.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(dp.y, dp.x) * Mathf.Rad2Deg); break; }
|
||
case "normal": rt.sizeDelta = Vector2.one * 0.45f * tw; im.color = new Color32(255, 240, 160, 255); break;
|
||
case "minion": rt.sizeDelta = Vector2.one * 0.4f * tw; im.color = new Color32(255, 90, 90, 255); break;
|
||
default: rt.sizeDelta = Vector2.one * 0.6f * tw; im.color = sh.unitId == 4 ? new Color32(255, 120, 255, 255) : (sh.unitId == 6 ? new Color32(255, 140, 60, 255) : new Color32(220, 255, 220, 255)); break;
|
||
}
|
||
}
|
||
// 순간 효과 — 레이저 · 번개 · 폭발 · 소환
|
||
while (fxViews.Count < B.Fx.Count) fxViews.Add(UI.Img(fxLayerB, "Fx", Art.SoftCircle, Color.white));
|
||
for (int i = 0; i < fxViews.Count; i++)
|
||
{
|
||
bool on = i < B.Fx.Count; fxViews[i].gameObject.SetActive(on); if (!on) continue;
|
||
var f = B.Fx[i]; var im = fxViews[i]; var rt = im.rectTransform;
|
||
float k = Mathf.Clamp01(f.t / Mathf.Max(0.01f, f.life));
|
||
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f); rt.localEulerAngles = Vector3.zero;
|
||
switch (f.type)
|
||
{
|
||
case "laser":
|
||
{
|
||
var d = TileVec(f.b - f.a); // 화면 px(세로가 짧은 땅) 기준 길이·각도
|
||
im.sprite = BeamSprite(); rt.pivot = new Vector2(0f, 0.5f);
|
||
rt.anchoredPosition = TileToLocal(f.a); rt.sizeDelta = new Vector2(d.magnitude, f.r * tw * (1.2f - 0.5f * k));
|
||
rt.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg);
|
||
im.color = new Color(1f, 1f, 1f, 1f - k * 0.7f);
|
||
break;
|
||
}
|
||
case "lightning":
|
||
{
|
||
var d = TileVec(f.a - f.b);
|
||
im.sprite = BoltSprite(); rt.pivot = new Vector2(0.5f, 0f);
|
||
rt.anchoredPosition = TileToLocal(f.b); rt.sizeDelta = new Vector2(1.75f * tw, d.magnitude);
|
||
rt.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg - 90f);
|
||
im.color = new Color(1f, 1f, 1f, 1f - k * 0.6f);
|
||
break;
|
||
}
|
||
case "blast":
|
||
im.sprite = Art.SoftCircle; rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = TileToLocal(f.a); rt.sizeDelta = TileVec(Vector2.one * f.r * 2f) * (1.3f + 0.4f * k); // 땅 위 타원 · 부드러운 원이라 1.3배 = 눈에 보이는 크기 ≈ 피해 반경
|
||
im.color = new Color(1f, 0.5f, 0.05f, 1f - 0.8f * k);
|
||
break;
|
||
case "heal": // 성벽 회복 — 초록 빛이 성을 감쌈
|
||
im.sprite = Art.SoftCircle; rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = TileToLocal(f.a); rt.sizeDelta = TileVec(Vector2.one * f.r * 2f) * (1.1f + 0.3f * k);
|
||
im.color = new Color(0.35f, 1f, 0.45f, 0.75f * (1f - k));
|
||
break;
|
||
default: // spawn
|
||
im.sprite = RingSprite(); rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = TileToLocal(f.a); rt.sizeDelta = TileVec(Vector2.one * f.r * 2f) * (0.6f + 0.6f * k);
|
||
im.color = new Color(0.7f, 0.9f, 1f, 0.9f * (1f - k));
|
||
break;
|
||
}
|
||
}
|
||
// 결과 덮개(승리·패배만 — 시작 정보는 스테이지 선택 화면의 지도 버튼)
|
||
overlay.gameObject.SetActive(B.State == BattleState.Won || B.State == BattleState.Lost);
|
||
overlayRibbon.sprite = Skin.Ribbon(B.State == BattleState.Won ? "Yellow" : B.State == BattleState.Lost ? "Red" : "Blue");
|
||
if (B.State == BattleState.Won)
|
||
{
|
||
bool next = NextOpen(B);
|
||
overlayTitle.text = "승리!";
|
||
overlayInfo.text = $"스테이지 {B.Stage}. {B.C.StageName(B.Stage)} 클리어" + (B.IsTest ? "\n(시험 전투 — 클리어 기록 안 함)" : next && G.StageFocus == B.Stage + 1 ? $"\n스테이지 {B.Stage + 1} 열림!" : B.Stage >= G.StageCount ? "\n모든 스테이지 클리어!" : "") + $"\n보상 {Fmt.Num(lastBattleReward)} 코인";
|
||
overlayBtnLabel.text = next ? "다음 스테이지" : "다시 하기";
|
||
}
|
||
else if (B.State == BattleState.Lost) { overlayTitle.text = "패배"; overlayInfo.text = $"성 체력 {Mathf.CeilToInt(B.CastleHp)} 남음\n강화 후 다시 도전하세요"; overlayBtnLabel.text = "다시 도전"; }
|
||
}
|
||
}
|
||
}
|