ThreeKingdomsSlingshot/Assets/Scripts/Runtime/UnitView.cs

232 lines
9.9 KiB
C#
Raw 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 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;
// HP 바 = 도형이면 원 위 · 아트면 머리 위(발 = −0.55 R · 세로 ArtHeight) — 얼굴을 가리지 않게(PD #1081 첨부 캡처)
float topY = anim != null ? -u.Radius * 0.55f + u.ArtHeight + 0.12f : u.Radius + 0.22f;
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, topY, 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, topY + 0.24f, 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);
}
}
}