ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Gimmicks/GimmickView.cs

352 lines
15 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 기믹 표시(PD #1097 §6): 스프라이트(Resources/Gimmicks/Art/&lt;art&gt;)가 있으면 모양에 맞춰 늘리고, 없으면 색 도형 + 이름 글자.
/// 장애물 = 내구 핍 · 피격 흔들림 · 파괴 파편 → 잔해(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);
}
}
}