ThreeKingdomsSlingshot/Assets/Scripts/Runtime/UnitView.cs

230 lines
9.6 KiB
C#

using System.Collections;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 유닛 표시. 아트(Resources/Units/&lt;art&gt;/ 프레임 애니 · UnitArt)가 있으면 스프라이트(아군 = 뒷모습 back_* · 적군 = 앞모습 front_*),
/// 없으면 도형(몸통 원 + 병종 글자). 공통 = 이름 · HP 바 · 선택/표적 링 · 보스 테두리 · 창병 대기 반경.
/// </summary>
public class UnitView : MonoBehaviour
{
public static readonly Color PlayerColor = new Color(0.25f, 0.55f, 0.95f);
public static readonly Color EnemyColor = new Color(0.9f, 0.32f, 0.3f);
static readonly Color DimColor = new Color(0.3f, 0.3f, 0.32f);
static readonly Color ArtDimColor = new Color(0.45f, 0.45f, 0.5f);
static readonly Color ArtHitTint = new Color(1f, 0.45f, 0.45f);
const float HpBarWidth = 1.0f;
const float AttackHold = 0.35f;
const float HitHold = 0.3f;
Unit unit;
SpriteRenderer body;
FrameAnimator anim;
SpriteRenderer bossRing;
SpriteRenderer selectRing;
SpriteRenderer highlightRing;
SpriteRenderer overwatchArea;
SpriteRenderer hpBg;
SpriteRenderer hpFill;
TextMesh mark;
TextMesh nameLabel;
TextMesh badge;
Color baseColor;
Coroutine flash;
bool aiming;
/// <summary>스프라이트 아트로 그리는가(false = 도형).</summary>
public bool HasArt
{
get { return anim != null; }
}
public FrameAnimator Anim
{
get { return anim; }
}
public void Build(Unit u)
{
unit = u;
foreach (Transform c in transform) Destroy(c.gameObject);
baseColor = u.Faction == Faction.Player ? PlayerColor : EnemyColor;
float d = u.Radius * 2f;
// 창병 대기 반경 표시(창병만 · 발동 중에만 오브젝트 통째로 켬)
overwatchArea = ProceduralSprites.NewSprite(transform, "OverwatchArea", ProceduralSprites.Circle, new Color(0.3f, 0.65f, 1f, 0.16f), u.Type == UnitType.Spear ? u.AttackRange * 2f : 0f, -6);
var owRing = ProceduralSprites.NewSprite(overwatchArea.transform, "Ring", ProceduralSprites.Ring, new Color(0.5f, 0.8f, 1f, 0.6f), 1f, -5);
owRing.transform.localScale = Vector3.one;
overwatchArea.gameObject.SetActive(false);
if (u.IsBoss)
{
bossRing = ProceduralSprites.NewSprite(transform, "BossRing", ProceduralSprites.Circle, new Color(1f, 0.85f, 0.3f), d + 0.16f, -1);
}
body = ProceduralSprites.NewSprite(transform, "Body", ProceduralSprites.Circle, baseColor, d, 0);
// 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽)
anim = null;
var art = UnitArt.Load(u.ArtId);
if (art != null)
{
body.enabled = false;
var ago = new GameObject("Art");
ago.transform.SetParent(transform, false);
ago.transform.localPosition = new Vector3(0f, -u.Radius * 0.55f, 0f);
anim = ago.AddComponent<FrameAnimator>();
anim.Setup(art, u.ArtHeight, 0);
anim.SetIdle(ClipName("idle"));
anim.PlayIdle();
}
selectRing = ProceduralSprites.NewSprite(transform, "SelectRing", ProceduralSprites.Ring, new Color(1f, 0.95f, 0.4f), d + 0.28f, 1);
selectRing.enabled = false;
highlightRing = ProceduralSprites.NewSprite(transform, "HighlightRing", ProceduralSprites.Ring, new Color(1f, 0.5f, 0.15f), d + 0.28f, 1);
highlightRing.enabled = false;
hpBg = ProceduralSprites.NewSprite(transform, "HpBg", ProceduralSprites.Square, new Color(0.08f, 0.08f, 0.08f, 0.9f), 1f, 2);
hpBg.transform.localPosition = new Vector3(0f, u.Radius + 0.22f, 0f);
hpBg.transform.localScale = new Vector3(HpBarWidth, 0.12f, 1f);
hpFill = ProceduralSprites.NewSprite(transform, "HpFill", ProceduralSprites.Square, new Color(0.35f, 0.9f, 0.35f), 1f, 3);
hpFill.transform.localPosition = hpBg.transform.localPosition;
hpFill.transform.localScale = new Vector3(HpBarWidth, 0.12f, 1f);
mark = TextUtil.CreateWorldText(transform, UnitTypeInfo.Mark(u.Type), u.Radius * 1.1f, Color.white, 4);
if (anim != null) mark.gameObject.SetActive(false); // 아트가 있으면 병종은 이름 옆에
nameLabel = TextUtil.CreateWorldText(transform, anim != null ? u.DisplayName + "·" + UnitTypeInfo.Mark(u.Type) : u.DisplayName, 0.26f, new Color(0.95f, 0.95f, 0.9f), 4);
nameLabel.transform.localPosition = new Vector3(0f, -u.Radius - 0.22f, 0f);
badge = TextUtil.CreateWorldText(transform, "추가 이동", 0.22f, new Color(1f, 0.95f, 0.4f), 5);
badge.transform.localPosition = new Vector3(0f, u.Radius + 0.46f, 0f);
badge.gameObject.SetActive(false);
Refresh();
}
/// <summary>보기(아군 = back · 적군 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기.</summary>
string ClipName(string suffix)
{
string v = unit.Faction == Faction.Player ? "back" : "front";
string o = unit.Faction == Faction.Player ? "front" : "back";
if (anim.Has(v + "_" + suffix)) return v + "_" + suffix;
if (anim.Has(o + "_" + suffix)) return o + "_" + suffix;
return anim.Has(v + "_idle") ? v + "_idle" : o + "_idle";
}
public void Refresh()
{
if (unit == null || hpFill == null) return;
float ratio = unit.MaxHp > 0 ? Mathf.Clamp01((float)unit.Hp / unit.MaxHp) : 0f;
hpFill.transform.localScale = new Vector3(HpBarWidth * ratio, 0.12f, 1f);
hpFill.transform.localPosition = new Vector3(-HpBarWidth * 0.5f + HpBarWidth * ratio * 0.5f, hpBg.transform.localPosition.y, 0f);
hpFill.color = ratio > 0.5f ? new Color(0.35f, 0.9f, 0.35f) : (ratio > 0.25f ? new Color(0.95f, 0.8f, 0.2f) : new Color(0.95f, 0.3f, 0.2f));
bool dim = unit.Faction == Faction.Player && unit.ActedThisTurn;
if (flash == null)
{
if (anim != null) anim.Renderer.color = dim ? Color.Lerp(Color.white, ArtDimColor, 0.6f) : Color.white;
else body.color = dim ? Color.Lerp(baseColor, DimColor, 0.6f) : baseColor;
}
mark.color = dim ? new Color(1f, 1f, 1f, 0.6f) : Color.white;
bool ow = unit.Overwatch && unit.Type == UnitType.Spear && unit.IsAlive;
if (overwatchArea.gameObject.activeSelf != ow) overwatchArea.gameObject.SetActive(ow);
badge.gameObject.SetActive(unit.BonusMoveAvailable && unit.IsAlive);
}
public void SetSelected(bool on)
{
if (selectRing != null) selectRing.enabled = on;
}
public void SetHighlight(bool on)
{
if (highlightRing != null) highlightRing.enabled = on;
}
/// <summary>조준(당기는) 중 = 준비 자세 · 끝나면 대기.</summary>
public void SetAiming(bool on)
{
if (anim == null || aiming == on) return;
aiming = on;
if (on) anim.Play(ClipName("ready"), true);
else anim.PlayIdle();
}
/// <summary>공격 자세(1 프레임) 잠깐 → 대기.</summary>
public void PlayAttack()
{
if (anim == null || !isActiveAndEnabled) return;
aiming = false;
anim.Play(ClipName("action"), false, AttackHold, true);
}
/// <summary>승리 동작(앞모습 · 끝 프레임 유지).</summary>
public void PlayVictory()
{
if (anim == null || !isActiveAndEnabled || !unit.IsAlive) return;
aiming = false;
anim.Play(ClipName("victory"), false);
}
public void OnHit()
{
if (!isActiveAndEnabled) return;
if (flash != null) StopCoroutine(flash);
flash = StartCoroutine(FlashRoutine());
}
IEnumerator FlashRoutine()
{
if (anim != null)
{
anim.Renderer.color = ArtHitTint;
if (unit.IsAlive) anim.Play(ClipName("damage"), false, HitHold, true);
}
else
{
body.color = Color.white;
}
float t = 0f;
while (t < 0.18f)
{
t += Time.deltaTime;
yield return null;
}
flash = null;
Refresh();
}
public void PlayDeath()
{
if (!isActiveAndEnabled) return;
StartCoroutine(DeathRoutine());
}
IEnumerator DeathRoutine()
{
selectRing.enabled = false;
highlightRing.enabled = false;
overwatchArea.gameObject.SetActive(false);
if (anim != null) anim.Play(ClipName("damage"), false, 99f, false);
float t = 0f;
const float dur = 0.45f;
Vector3 s0 = transform.localScale;
var sr = anim != null ? anim.Renderer : body;
while (t < dur)
{
t += Time.deltaTime;
float k = Mathf.Clamp01(t / dur);
transform.localScale = s0 * (1f - 0.5f * k);
var c = sr.color;
c.a = 1f - k;
sr.color = c;
yield return null;
}
unit.gameObject.SetActive(false);
}
}
}