using System.Collections; using System.Collections.Generic; using UnityEngine; namespace TKS { /// /// 기믹 표시(PD #1097 §6): 스프라이트(Resources/Gimmicks/Art/<art>)가 있으면 모양에 맞춰 늘리고, 없으면 색 도형 + 이름 글자. /// 장애물 = 내구 핍 · 피격 흔들림 · 파괴 파편 → 잔해(art_broken) · 함정 = 발동 전 숨김 · 불 = 꺼지면 회색 · 군기 = 점령 진영 색. /// 정렬 = 배경(−20~−16) 위 · 유닛(−6~) 아래. /// public class GimmickView { public const int ZoneOrder = -15; public const int RiverOrder = -14; public const int BridgeOrder = -13; public const int BlockOrder = -12; public const int LabelOrder = -11; public const string ArtFolder = "Gimmicks/Art/"; static Material polyMaterial; readonly GimmickInstance g; readonly Transform root; Transform body; Renderer fill; TextMesh label; readonly List pips = new List(); Color baseColor; Coroutine shake; public GimmickView(GimmickInstance inst) { g = inst; root = inst.Go.transform; Build(); } public Transform Body { get { return body; } } public bool Visible { get { return body != null && body.gameObject.activeSelf; } } public string LabelText { get { return label != null ? label.text : ""; } } public int PipCount { get { return pips.Count; } } public static Color ColorOf(GimmickDef d) { switch (d.zone) { case GimmickZone.Poison: return new Color(0.55f, 0.25f, 0.75f, 0.45f); case GimmickZone.Slow: return new Color(0.42f, 0.32f, 0.16f, 0.5f); case GimmickZone.Burn: return new Color(1f, 0.45f, 0.1f, 0.5f); case GimmickZone.Slide: return new Color(0.6f, 0.88f, 1f, 0.5f); case GimmickZone.High: return new Color(0.78f, 0.66f, 0.4f, 0.38f); case GimmickZone.Fog: return new Color(0.92f, 0.92f, 0.96f, 0.42f); case GimmickZone.Capture: return new Color(0.75f, 0.75f, 0.75f, 0.3f); case GimmickZone.Trap: return new Color(0.3f, 0.18f, 0.08f, 0.85f); case GimmickZone.River: return new Color(0.2f, 0.45f, 0.85f, 0.9f); } if (d.id == GimmickIds.Bridge) return new Color(0.62f, 0.46f, 0.26f, 1f); if (d.id == GimmickIds.Boulder) return new Color(0.52f, 0.52f, 0.55f, 1f); if (d.projectile == GimmickProjectile.Destroy) return new Color(0.78f, 0.22f, 0.15f, 1f); if (d.strategistClear) return new Color(0.2f, 0.42f, 0.18f, 1f); // 가시덤불 if (d.projectile == GimmickProjectile.Pass) return new Color(0.58f, 0.4f, 0.2f, 1f); // 목책 if (d.durable) return new Color(0.42f, 0.27f, 0.14f, 1f); // 성문 return new Color(0.46f, 0.46f, 0.43f, 1f); // 성벽 } static int OrderOf(GimmickInstance i) { if (i.Zone == GimmickZone.River) return RiverOrder; if (i.IsBridge) return BridgeOrder; return i.IsBlock ? BlockOrder : ZoneOrder; } void Build() { var bgo = new GameObject("Body"); bgo.transform.SetParent(root, false); body = bgo.transform; baseColor = ColorOf(g.Def); int order = OrderOf(g); var s = g.Shape; var art = string.IsNullOrEmpty(g.Def.art) ? null : Resources.Load(ArtFolder + g.Def.art); if (s.Type == GimmickShapeType.Poly) { fill = MakePoly(body, s, baseColor, order); } else { Vector2 size = s.Type == GimmickShapeType.Circle ? new Vector2(s.R * 2f, s.R * 2f) : s.Size; var sr = ProceduralSprites.NewSprite(body, "Fill", art != null ? art : (s.Type == GimmickShapeType.Circle ? ProceduralSprites.Circle : ProceduralSprites.Square), art != null ? Color.white : baseColor, 1f, order); Vector2 native = sr.sprite.bounds.size; sr.transform.localScale = new Vector3(size.x / Mathf.Max(0.001f, native.x), size.y / Mathf.Max(0.001f, native.y), 1f); sr.transform.localRotation = Quaternion.Euler(0f, 0f, s.Type == GimmickShapeType.Rect ? s.Rot : 0f); fill = sr; if (art != null) baseColor = Color.white; if (g.Zone == GimmickZone.Fog) sr.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; // 책사 광역이 걷은 자리(SpriteMask)는 안 그림 } float h = Mathf.Clamp(Mathf.Min(s.Type == GimmickShapeType.Rect ? Mathf.Min(s.Size.x, s.Size.y) : s.BoundingRadius) * 0.55f, 0.22f, 0.5f); label = TextUtil.CreateWorldText(body, g.Def.nameKo, h, new Color(1f, 1f, 1f, g.IsBlock ? 0.95f : 0.75f), LabelOrder); label.gameObject.name = "Label"; if (g.Zone == GimmickZone.River && s.Type == GimmickShapeType.Rect) label.transform.localPosition = (Vector3)(s.LocalAxisX * s.Size.x * 0.3f); // 다리 글자와 겹치지 않게 if (g.Def.durable && g.MaxHp > 0) { float top = s.Type == GimmickShapeType.Rect ? Mathf.Abs(s.Size.y) * 0.5f + 0.18f : s.BoundingRadius + 0.18f; const float w = 0.16f, gap = 0.05f; float total = g.MaxHp * w + (g.MaxHp - 1) * gap; for (int i = 0; i < g.MaxHp; i++) { var p = ProceduralSprites.NewSprite(body, "Pip", ProceduralSprites.Square, Color.white, 1f, LabelOrder); p.transform.localPosition = new Vector3(-total * 0.5f + w * 0.5f + i * (w + gap), top, 0f); p.transform.localScale = new Vector3(w, 0.1f, 1f); var ps = UISkin.WorldSprite("pip", 460f); // 팩 둥근 칸 핍(PD #1100 · 외곽선) if (ps != null) { p.sprite = ps; p.drawMode = SpriteDrawMode.Sliced; p.transform.localScale = Vector3.one; p.size = new Vector2(w, 0.12f); } pips.Add(p); } } if (g.Zone == GimmickZone.Trap) body.gameObject.SetActive(false); // 함정 = 숨김(아군 조준선 미표시 · 적 AI 는 인지) Refresh(); } static Renderer MakePoly(Transform parent, GimmickShape s, Color color, int order) { var go = new GameObject("Fill"); go.transform.SetParent(parent, false); var mf = go.AddComponent(); var mr = go.AddComponent(); if (polyMaterial == null) polyMaterial = new Material(Shader.Find("Sprites/Default")); mr.sharedMaterial = polyMaterial; mr.sortingOrder = order; var mesh = new Mesh(); var verts = new Vector3[s.Points.Length]; var cols = new Color[s.Points.Length]; for (int i = 0; i < verts.Length; i++) { verts[i] = s.Points[i] - s.Center; cols[i] = color; } mesh.vertices = verts; mesh.colors = cols; mesh.triangles = Triangulate(s.Points); mf.sharedMesh = mesh; return mr; } /// 귀 자르기 삼각 분할(단순 다각형). public static int[] Triangulate(Vector2[] pts) { var tris = new List(); var idx = new List(); float area = 0f; for (int i = 0, j = pts.Length - 1; i < pts.Length; j = i++) area += pts[j].x * pts[i].y - pts[i].x * pts[j].y; for (int i = 0; i < pts.Length; i++) idx.Add(area > 0f ? i : pts.Length - 1 - i); int guard = 0; while (idx.Count > 3 && guard++ < 1000) { bool cut = false; for (int i = 0; i < idx.Count; i++) { int a = idx[(i + idx.Count - 1) % idx.Count], b = idx[i], c = idx[(i + 1) % idx.Count]; Vector2 pa = pts[a], pb = pts[b], pc = pts[c]; if ((pb.x - pa.x) * (pc.y - pa.y) - (pb.y - pa.y) * (pc.x - pa.x) <= 0f) continue; bool inside = false; foreach (int k in idx) { if (k == a || k == b || k == c) continue; if (InTri(pts[k], pa, pb, pc)) { inside = true; break; } } if (inside) continue; tris.Add(a); tris.Add(b); tris.Add(c); idx.RemoveAt(i); cut = true; break; } if (!cut) break; } if (idx.Count == 3) { tris.Add(idx[0]); tris.Add(idx[1]); tris.Add(idx[2]); } return tris.ToArray(); } static bool InTri(Vector2 p, Vector2 a, Vector2 b, Vector2 c) { float d1 = (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y); float d2 = (p.x - c.x) * (b.y - c.y) - (b.x - c.x) * (p.y - c.y); float d3 = (p.x - a.x) * (c.y - a.y) - (c.x - a.x) * (p.y - a.y); bool neg = d1 < 0 || d2 < 0 || d3 < 0, pos = d1 > 0 || d2 > 0 || d3 > 0; return !(neg && pos); } /// 상태(내구 · 잔해 · 군기 · 불 꺼짐 · 함정 노출)를 그림에 반영. public void Refresh() { if (body == null) return; Color c = baseColor; string text = g.Def.nameKo; if (g.Broken) { c = Color.Lerp(baseColor, new Color(0.2f, 0.18f, 0.15f, 0.55f), 0.6f); text = g.Def.nameKo + " 잔해"; var broken = string.IsNullOrEmpty(g.Def.artBroken) ? null : Resources.Load(ArtFolder + g.Def.artBroken); var sr = fill as SpriteRenderer; if (broken != null && sr != null) { sr.sprite = broken; c = Color.white; } } if (g.Out) { c = new Color(0.35f, 0.33f, 0.3f, 0.35f); text = "꺼진 잔해"; } if (g.Zone == GimmickZone.Capture) { c = g.Owner == (int)Faction.Player ? new Color(0.25f, 0.55f, 0.95f, 0.35f) : g.Owner == (int)Faction.Enemy ? new Color(0.9f, 0.3f, 0.25f, 0.35f) : baseColor; text = g.Owner < 0 ? g.Def.nameKo : g.Def.nameKo + (g.Owner == (int)Faction.Player ? "(아군)" : "(적군)"); } if (g.Zone == GimmickZone.Burn && g.BurnLeft > 0) text = g.Def.nameKo + " " + g.BurnLeft; SetColor(c); if (label != null) label.text = text; for (int i = 0; i < pips.Count; i++) { pips[i].color = i < g.Hp ? UISkin.Col("pip_on", new Color(1f, 0.85f, 0.3f)) : UISkin.Col("pip_off", new Color(0.15f, 0.15f, 0.15f, 0.8f)); pips[i].gameObject.SetActive(!g.Broken); } } void SetColor(Color c) { var sr = fill as SpriteRenderer; if (sr != null) sr.color = c; var mr = fill as MeshRenderer; if (mr != null) { var mf = mr.GetComponent(); if (mf != null && mf.sharedMesh != null) { var cols = new Color[mf.sharedMesh.vertexCount]; for (int i = 0; i < cols.Length; i++) cols[i] = c; mf.sharedMesh.colors = cols; } } } /// 피격 흔들림. public void Shake() { if (body == null || Fx.Runner == null) return; if (shake != null) Fx.Runner.StopCoroutine(shake); shake = Fx.Run(ShakeRoutine()); } IEnumerator ShakeRoutine() { float t = 0f; const float dur = 0.25f; while (t < dur && body != null) { t += Time.deltaTime; float k = 1f - t / dur; body.localPosition = new Vector3(Mathf.Sin(t * 70f) * 0.09f * k, Mathf.Cos(t * 55f) * 0.05f * k, 0f); yield return null; } if (body != null) body.localPosition = Vector3.zero; shake = null; } /// 파괴 파편(작은 조각이 흩어지며 사라짐) → 잔해 표시. public void Shatter() { Fx.Run(ShatterRoutine(g.Shape.Center, baseColor)); Refresh(); } static IEnumerator ShatterRoutine(Vector2 c, Color color) { var bits = new List(); var vel = new List(); for (int i = 0; i < 10; i++) { var sr = ProceduralSprites.NewSprite(null, "Debris", ProceduralSprites.Square, new Color(color.r, color.g, color.b, 1f), 0.16f, 7); sr.transform.position = new Vector3(c.x, c.y, 0f); float a = i * Mathf.PI * 2f / 10f + Random.value * 0.4f; vel.Add(new Vector2(Mathf.Cos(a), Mathf.Sin(a)) * (1.5f + Random.value * 1.5f)); bits.Add(sr); } float t = 0f; const float dur = 0.5f; while (t < dur) { t += Time.deltaTime; for (int i = 0; i < bits.Count; i++) { bits[i].transform.position += (Vector3)(vel[i] * Time.deltaTime); var col = bits[i].color; col.a = 1f - t / dur; bits[i].color = col; } yield return null; } foreach (var b in bits) Object.Destroy(b.gameObject); } /// 함정 발동(드러났다 사라짐) · 덤불 제거(흐려지며 사라짐). public void Vanish(bool reveal) { if (body == null) return; if (reveal) body.gameObject.SetActive(true); Refresh(); Fx.Run(VanishRoutine(reveal ? 0.7f : 0.35f)); } IEnumerator VanishRoutine(float dur) { float t = 0f; while (t < dur && body != null) { t += Time.deltaTime; var c = baseColor; c.a = baseColor.a * (1f - t / dur); SetColor(c); yield return null; } if (body != null) body.gameObject.SetActive(false); } } }