ThreeKingdomsSlingshot/Assets/Scripts/Runtime/UnitView.cs

286 lines
12 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;
bool facingBack; // 위를 봄(뒷모습 back_* 세트) · false = 아래를 봄(앞모습 front_*)
bool facingLeft; // 왼쪽을 봄(좌우 반전) · false = 오른쪽(기본)
const float FacingEps = 0.05f;
/// <summary>스프라이트 아트로 그리는가(false = 도형).</summary>
public bool HasArt
{
get { return anim != null; }
}
public FrameAnimator Anim
{
get { return anim; }
}
/// <summary>4방향 보기(PD #1082): 위를 보면 뒷모습(↖·↗) · 아래를 보면 앞모습(↙·↘).</summary>
public bool FacingBack
{
get { return facingBack; }
}
/// <summary>왼쪽을 보면 좌우 반전(↙·↖) · 오른쪽이 기본(↘·↗).</summary>
public bool FacingLeft
{
get { return facingLeft; }
}
/// <summary>
/// 마지막 이동 방향으로 보는 방향을 정한다(PD #1082 · 슬링샷·튕김·밀림·돌진 뒤 마지막 방향).
/// 세로 성분이 있으면 앞/뒤 세트를, 가로 성분이 있으면 좌우 반전을 바꾼다(거의 0 인 축은 이전 값 유지).
/// </summary>
public void SetFacing(Vector2 dir)
{
bool viewChanged = false;
if (dir.y > FacingEps && !facingBack)
{
facingBack = true;
viewChanged = true;
}
else if (dir.y < -FacingEps && facingBack)
{
facingBack = false;
viewChanged = true;
}
if (dir.x > FacingEps) facingLeft = false;
else if (dir.x < -FacingEps) facingLeft = true;
if (anim == null) return;
anim.Renderer.flipX = facingLeft;
if (!viewChanged) return;
// 앞/뒤 세트가 바뀌면 지금 동작을 새 보기의 같은 클립으로 이어 간다
string cur = anim.Current ?? "";
string suffix = cur.StartsWith("back_") ? cur.Substring(5) : (cur.StartsWith("front_") ? cur.Substring(6) : "idle");
anim.SetIdle(ClipName("idle"));
if (suffix == "idle") anim.PlayIdle();
else if (suffix == "ready") anim.Play(ClipName("ready"), true);
else if (suffix == "victory") anim.Play(ClipName("victory"), false);
else anim.Play(ClipName(suffix), false, AttackHold, true);
}
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);
// 시작 보기: 아군 = 위(↗ 뒷모습) · 적군 = 아래(↘ 앞모습) · 이후는 마지막 이동 방향(PD #1082)
facingBack = u.Faction == Faction.Player;
facingLeft = false;
// 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽)
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.Renderer.flipX = facingLeft;
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 = facingBack ? "back" : "front";
string o = facingBack ? "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);
}
}
}