352 lines
15 KiB
C#
352 lines
15 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace TKS
|
||
{
|
||
/// <summary>
|
||
/// 기믹 표시(PD #1097 §6): 스프라이트(Resources/Gimmicks/Art/<art>)가 있으면 모양에 맞춰 늘리고, 없으면 색 도형 + 이름 글자.
|
||
/// 장애물 = 내구 핍 · 피격 흔들림 · 파괴 파편 → 잔해(art_broken) · 함정 = 발동 전 숨김 · 불 = 꺼지면 회색 · 군기 = 점령 진영 색.
|
||
/// 정렬 = 배경(−20~−16) 위 · 유닛(−6~) 아래.
|
||
/// </summary>
|
||
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<SpriteRenderer> pips = new List<SpriteRenderer>();
|
||
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<Sprite>(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<MeshFilter>();
|
||
var mr = go.AddComponent<MeshRenderer>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>귀 자르기 삼각 분할(단순 다각형).</summary>
|
||
public static int[] Triangulate(Vector2[] pts)
|
||
{
|
||
var tris = new List<int>();
|
||
var idx = new List<int>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>상태(내구 · 잔해 · 군기 · 불 꺼짐 · 함정 노출)를 그림에 반영.</summary>
|
||
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<Sprite>(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<MeshFilter>();
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>피격 흔들림.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>파괴 파편(작은 조각이 흩어지며 사라짐) → 잔해 표시.</summary>
|
||
public void Shatter()
|
||
{
|
||
Fx.Run(ShatterRoutine(g.Shape.Center, baseColor));
|
||
Refresh();
|
||
}
|
||
|
||
static IEnumerator ShatterRoutine(Vector2 c, Color color)
|
||
{
|
||
var bits = new List<SpriteRenderer>();
|
||
var vel = new List<Vector2>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>함정 발동(드러났다 사라짐) · 덤불 제거(흐려지며 사라짐).</summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|