2026-11-03 19:10:10 +00:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TKS
|
|
|
|
|
|
{
|
2026-11-04 04:05:46 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 유닛 표시. 아트(Resources/Units/<art>/ 프레임 애니 · UnitArt)가 있으면 스프라이트(아군 = 뒷모습 back_* · 적군 = 앞모습 front_*),
|
|
|
|
|
|
/// 없으면 도형(몸통 원 + 병종 글자). 공통 = 이름 · HP 바 · 선택/표적 링 · 보스 테두리 · 창병 대기 반경.
|
|
|
|
|
|
/// </summary>
|
2026-11-03 19:10:10 +00:00
|
|
|
|
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);
|
2026-11-04 04:05:46 +00:00
|
|
|
|
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);
|
2026-11-05 04:06:32 +00:00
|
|
|
|
static readonly Color HighlightColor = new Color(1f, 0.5f, 0.15f);
|
|
|
|
|
|
static readonly Color FriendlyFireRingColor = new Color(1f, 0.1f, 0.1f);
|
2026-11-04 04:05:46 +00:00
|
|
|
|
const float HpBarWidth = 1.0f;
|
|
|
|
|
|
const float AttackHold = 0.35f;
|
|
|
|
|
|
const float HitHold = 0.3f;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
|
|
Unit unit;
|
|
|
|
|
|
SpriteRenderer body;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
FrameAnimator anim;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
SpriteRenderer bossRing;
|
|
|
|
|
|
SpriteRenderer selectRing;
|
|
|
|
|
|
SpriteRenderer highlightRing;
|
|
|
|
|
|
SpriteRenderer overwatchArea;
|
|
|
|
|
|
SpriteRenderer hpBg;
|
|
|
|
|
|
SpriteRenderer hpFill;
|
2026-11-06 14:20:57 +00:00
|
|
|
|
bool packHp;
|
|
|
|
|
|
// 체력 바 9-slice 월드 배율(테두리 px → 월드 = px / ppu · 바 높이 0.22 / 0.13 안에 테두리가 들어가게)
|
|
|
|
|
|
const float HpBgPpu = 240f;
|
|
|
|
|
|
const float HpFillPpu = 420f;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
TextMesh mark;
|
|
|
|
|
|
TextMesh nameLabel;
|
|
|
|
|
|
TextMesh badge;
|
2026-11-05 14:31:27 +00:00
|
|
|
|
TextMesh skillBadge; // 예약한 액티브(next_action) 이름(PD #1093 B)
|
2026-11-03 19:10:10 +00:00
|
|
|
|
Color baseColor;
|
|
|
|
|
|
Coroutine flash;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
bool aiming;
|
2026-11-05 04:06:32 +00:00
|
|
|
|
bool highlightFriendly;
|
2026-11-04 15:56:38 +00:00
|
|
|
|
bool charging; // 슬링샷 돌진(이동) 중 = 돌진 포즈 고정
|
2026-11-04 08:58:30 +00:00
|
|
|
|
bool facingBack; // 위를 봄(뒷모습 back_* 세트) · false = 아래를 봄(앞모습 front_*)
|
|
|
|
|
|
bool facingLeft; // 왼쪽을 봄(좌우 반전) · false = 오른쪽(기본)
|
|
|
|
|
|
const float FacingEps = 0.05f;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>스프라이트 아트로 그리는가(false = 도형).</summary>
|
|
|
|
|
|
public bool HasArt
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return anim != null; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public FrameAnimator Anim
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return anim; }
|
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
2026-11-04 08:58:30 +00:00
|
|
|
|
/// <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)
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-04 08:58:30 +00:00
|
|
|
|
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"));
|
2026-11-04 15:56:38 +00:00
|
|
|
|
if (charging) anim.Play(ClipName("action"), true);
|
|
|
|
|
|
else if (suffix == "idle") anim.PlayIdle();
|
2026-11-04 08:58:30 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-10 04:19:44 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 되돌아가기(PD 2026-11-10 「기병이 공격 후 복귀할 때 방향 전환 · 이미지 리소스 반전」): 보는 방향을 dir 로 바꾸고,
|
|
|
|
|
|
/// dir 에 가로 성분이 없어 좌우가 그대로면(곧장 위·아래로 돌아감) 좌우 반전을 뒤집는다 — 돌아서는 모습이 늘 보이게.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void TurnAround(Vector2 dir)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Sim.Active) return;
|
|
|
|
|
|
bool sideways = Mathf.Abs(dir.x) > FacingEps;
|
|
|
|
|
|
SetFacing(dir);
|
|
|
|
|
|
if (sideways) return;
|
|
|
|
|
|
facingLeft = !facingLeft;
|
|
|
|
|
|
if (anim != null) anim.Renderer.flipX = facingLeft;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-11-03 19:29:16 +00:00
|
|
|
|
// 창병 대기 반경 표시(창병만 · 발동 중에만 오브젝트 통째로 켬)
|
|
|
|
|
|
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);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
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;
|
2026-11-03 19:29:16 +00:00
|
|
|
|
overwatchArea.gameObject.SetActive(false);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-11-04 08:58:30 +00:00
|
|
|
|
// 시작 보기: 아군 = 위(↗ 뒷모습) · 적군 = 아래(↘ 앞모습) · 이후는 마지막 이동 방향(PD #1082)
|
|
|
|
|
|
facingBack = u.Faction == Faction.Player;
|
|
|
|
|
|
facingLeft = false;
|
|
|
|
|
|
|
2026-11-04 04:05:46 +00:00
|
|
|
|
// 아트가 있으면 몸통 원 대신 스프라이트 애니(발 = 원 아래쪽)
|
|
|
|
|
|
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>();
|
2026-11-09 10:33:00 +00:00
|
|
|
|
anim.Setup(art, u.ArtHeight, 0, u.MountedArtMaxTop);
|
2026-11-09 10:23:50 +00:00
|
|
|
|
u.ArtHeight = anim.TopHeight; // 크기 표 배율 뒤 실제 머리 끝(HP 바 · 머리 위 표시 기준)
|
2026-11-04 08:58:30 +00:00
|
|
|
|
anim.Renderer.flipX = facingLeft;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
anim.SetIdle(ClipName("idle"));
|
|
|
|
|
|
anim.PlayIdle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
|
selectRing = ProceduralSprites.NewSprite(transform, "SelectRing", ProceduralSprites.Ring, new Color(1f, 0.95f, 0.4f), d + 0.28f, 1);
|
|
|
|
|
|
selectRing.enabled = false;
|
2026-11-05 04:06:32 +00:00
|
|
|
|
highlightRing = ProceduralSprites.NewSprite(transform, "HighlightRing", ProceduralSprites.Ring, HighlightColor, d + 0.28f, 1);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
highlightRing.enabled = false;
|
|
|
|
|
|
|
2026-11-04 08:32:07 +00:00
|
|
|
|
// HP 바 = 도형이면 원 위 · 아트면 머리 위(발 = −0.55 R · 세로 ArtHeight) — 얼굴을 가리지 않게(PD #1081 첨부 캡처)
|
|
|
|
|
|
float topY = anim != null ? -u.Radius * 0.55f + u.ArtHeight + 0.12f : u.Radius + 0.22f;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
hpBg = ProceduralSprites.NewSprite(transform, "HpBg", ProceduralSprites.Square, new Color(0.08f, 0.08f, 0.08f, 0.9f), 1f, 2);
|
2026-11-04 08:32:07 +00:00
|
|
|
|
hpBg.transform.localPosition = new Vector3(0f, topY, 0f);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
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);
|
2026-11-06 14:20:57 +00:00
|
|
|
|
// 팩 슬라이더 체력 바(PD #1100 · 테마 hp_bg·hp_fill · 9-slice 월드 사본) — 테마가 없으면 위 도형 그대로
|
|
|
|
|
|
var bgSp = UISkin.WorldSprite("hp_bg", HpBgPpu);
|
|
|
|
|
|
var fillSp = UISkin.WorldSprite("hp_fill", HpFillPpu);
|
|
|
|
|
|
if (bgSp != null && fillSp != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
packHp = true;
|
|
|
|
|
|
hpBg.sprite = bgSp;
|
|
|
|
|
|
hpBg.drawMode = SpriteDrawMode.Sliced;
|
|
|
|
|
|
hpBg.transform.localScale = Vector3.one;
|
|
|
|
|
|
hpBg.size = new Vector2(HpBarWidth + 0.1f, 0.22f);
|
|
|
|
|
|
hpBg.color = UISkin.Col("hp_bg", Color.white);
|
|
|
|
|
|
hpFill.sprite = fillSp;
|
|
|
|
|
|
hpFill.drawMode = SpriteDrawMode.Sliced;
|
|
|
|
|
|
hpFill.transform.localScale = Vector3.one;
|
|
|
|
|
|
hpFill.size = new Vector2(HpBarWidth, 0.13f);
|
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
|
|
mark = TextUtil.CreateWorldText(transform, UnitTypeInfo.Mark(u.Type), u.Radius * 1.1f, Color.white, 4);
|
2026-11-04 04:05:46 +00:00
|
|
|
|
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);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
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);
|
2026-11-04 08:32:07 +00:00
|
|
|
|
badge.transform.localPosition = new Vector3(0f, topY + 0.24f, 0f);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
badge.gameObject.SetActive(false);
|
2026-11-05 14:31:27 +00:00
|
|
|
|
skillBadge = TextUtil.CreateWorldText(transform, "", 0.24f, SkillFx.NameColor, 5);
|
|
|
|
|
|
skillBadge.transform.localPosition = new Vector3(0f, topY + 0.5f, 0f);
|
|
|
|
|
|
skillBadge.gameObject.SetActive(false);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 14:31:27 +00:00
|
|
|
|
/// <summary>예약 표시 중인가(테스트·캡처 확인용).</summary>
|
|
|
|
|
|
public bool SkillBadgeVisible
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return skillBadge != null && skillBadge.gameObject.activeSelf; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 08:58:30 +00:00
|
|
|
|
/// <summary>보는 방향(위 = back · 아래 = front)에 맞는 클립 이름. 그 보기에 없으면 다른 보기 → 대기.</summary>
|
2026-11-04 04:05:46 +00:00
|
|
|
|
string ClipName(string suffix)
|
|
|
|
|
|
{
|
2026-11-04 08:58:30 +00:00
|
|
|
|
string v = facingBack ? "back" : "front";
|
|
|
|
|
|
string o = facingBack ? "front" : "back";
|
2026-11-04 04:05:46 +00:00
|
|
|
|
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";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
|
public void Refresh()
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-03 19:10:10 +00:00
|
|
|
|
if (unit == null || hpFill == null) return;
|
|
|
|
|
|
float ratio = unit.MaxHp > 0 ? Mathf.Clamp01((float)unit.Hp / unit.MaxHp) : 0f;
|
2026-11-06 14:20:57 +00:00
|
|
|
|
if (packHp)
|
|
|
|
|
|
{
|
|
|
|
|
|
float w = HpBarWidth * ratio;
|
|
|
|
|
|
hpFill.enabled = w > 0.045f;
|
|
|
|
|
|
hpFill.size = new Vector2(Mathf.Max(0.045f, w), 0.13f);
|
|
|
|
|
|
hpFill.transform.localPosition = new Vector3(-HpBarWidth * 0.5f + w * 0.5f, hpBg.transform.localPosition.y + 0.015f, 0f);
|
|
|
|
|
|
hpFill.color = ratio > 0.5f ? UISkin.Col("hp_hi", Color.green) : ratio > 0.25f ? UISkin.Col("hp_mid", Color.yellow) : UISkin.Col("hp_low", Color.red);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
|
|
bool dim = unit.Faction == Faction.Player && unit.ActedThisTurn;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
|
mark.color = dim ? new Color(1f, 1f, 1f, 0.6f) : Color.white;
|
|
|
|
|
|
|
2026-11-03 19:29:16 +00:00
|
|
|
|
bool ow = unit.Overwatch && unit.Type == UnitType.Spear && unit.IsAlive;
|
2026-11-05 14:31:27 +00:00
|
|
|
|
if (ow)
|
|
|
|
|
|
{
|
2026-11-10 10:00:18 +00:00
|
|
|
|
float d = unit.AttackRange * 2f; // 옛 견제 반경(대기 유닛 없음 · 2026-11-10 #1112 스킬 확장 제거)
|
2026-11-05 14:31:27 +00:00
|
|
|
|
overwatchArea.transform.localScale = new Vector3(d, d, 1f);
|
|
|
|
|
|
}
|
2026-11-03 19:29:16 +00:00
|
|
|
|
if (overwatchArea.gameObject.activeSelf != ow) overwatchArea.gameObject.SetActive(ow);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
badge.gameObject.SetActive(unit.BonusMoveAvailable && unit.IsAlive);
|
2026-11-05 14:31:27 +00:00
|
|
|
|
|
|
|
|
|
|
var sk = SkillSystem.Current != null ? SkillSystem.Current.Of(unit) : null;
|
|
|
|
|
|
bool reserved = sk != null && sk.Reserved && sk.Active != null && unit.IsAlive;
|
|
|
|
|
|
if (reserved) skillBadge.text = "★" + sk.Active.NameKo;
|
|
|
|
|
|
if (skillBadge.gameObject.activeSelf != reserved) skillBadge.gameObject.SetActive(reserved);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void SetSelected(bool on)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (selectRing != null) selectRing.enabled = on;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void SetHighlight(bool on)
|
|
|
|
|
|
{
|
2026-11-05 04:06:32 +00:00
|
|
|
|
SetHighlight(on, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>공격 예정 표시 링 · friendly = 궁병·책사 사격에 맞을 같은 편(붉게 · PD 2026-11-05 #1087 ⑤⑥).</summary>
|
|
|
|
|
|
public void SetHighlight(bool on, bool friendly)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (highlightRing == null) return;
|
|
|
|
|
|
highlightRing.enabled = on;
|
|
|
|
|
|
highlightRing.color = friendly ? FriendlyFireRingColor : HighlightColor;
|
|
|
|
|
|
highlightFriendly = on && friendly;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool Highlighted
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return highlightRing != null && highlightRing.enabled; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>오사 경고(붉은 링) 중인가.</summary>
|
|
|
|
|
|
public bool HighlightFriendly
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return highlightFriendly; }
|
2026-11-03 19:10:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:56:38 +00:00
|
|
|
|
/// <summary>조준(당기는) 중 = 출발 자세(ready · 1프레임) · 끝나면 대기 (PD 2026-11-05 #1084).</summary>
|
2026-11-04 04:05:46 +00:00
|
|
|
|
public void SetAiming(bool on)
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-04 04:05:46 +00:00
|
|
|
|
if (anim == null || aiming == on) return;
|
|
|
|
|
|
aiming = on;
|
|
|
|
|
|
if (on) anim.Play(ClipName("ready"), true);
|
2026-11-04 15:56:38 +00:00
|
|
|
|
else if (!charging) anim.PlayIdle();
|
2026-11-04 04:05:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:56:38 +00:00
|
|
|
|
/// <summary>슬링샷 돌진(이동) 중 = 병종 돌진 포즈(action · 1프레임 고정) · 멈추면 대기 (PD 2026-11-05 #1084).</summary>
|
|
|
|
|
|
public void SetCharging(bool on)
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-04 15:56:38 +00:00
|
|
|
|
if (anim == null || charging == on) return;
|
|
|
|
|
|
charging = on;
|
|
|
|
|
|
aiming = false;
|
|
|
|
|
|
if (on) anim.Play(ClipName("action"), true);
|
|
|
|
|
|
else if (unit != null && unit.IsAlive) anim.PlayIdle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool Charging
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return charging; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>공격 자세(1 프레임) 잠깐 → 대기. 돌진 중이면 이미 돌진 포즈라 그대로 둔다.</summary>
|
2026-11-04 04:05:46 +00:00
|
|
|
|
public void PlayAttack()
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-04 15:56:38 +00:00
|
|
|
|
if (anim == null || !isActiveAndEnabled || charging) return;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
aiming = false;
|
|
|
|
|
|
anim.Play(ClipName("action"), false, AttackHold, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>승리 동작(앞모습 · 끝 프레임 유지).</summary>
|
|
|
|
|
|
public void PlayVictory()
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-04 04:05:46 +00:00
|
|
|
|
if (anim == null || !isActiveAndEnabled || !unit.IsAlive) return;
|
|
|
|
|
|
aiming = false;
|
|
|
|
|
|
anim.Play(ClipName("victory"), false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
|
public void OnHit()
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-03 19:10:10 +00:00
|
|
|
|
if (!isActiveAndEnabled) return;
|
|
|
|
|
|
if (flash != null) StopCoroutine(flash);
|
|
|
|
|
|
flash = StartCoroutine(FlashRoutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator FlashRoutine()
|
|
|
|
|
|
{
|
2026-11-04 04:05:46 +00:00
|
|
|
|
if (anim != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
anim.Renderer.color = ArtHitTint;
|
2026-11-04 15:56:38 +00:00
|
|
|
|
if (unit.IsAlive && !charging) anim.Play(ClipName("damage"), false, HitHold, true); // 돌진 중 피격은 색만(포즈 유지)
|
2026-11-04 04:05:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
body.color = Color.white;
|
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
|
float t = 0f;
|
|
|
|
|
|
while (t < 0.18f)
|
|
|
|
|
|
{
|
|
|
|
|
|
t += Time.deltaTime;
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
flash = null;
|
|
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void PlayDeath()
|
|
|
|
|
|
{
|
2026-11-05 16:20:07 +00:00
|
|
|
|
if (Sim.Active) return; // 가상 실행(적 AI 예측) = 표시 그대로
|
2026-11-03 19:10:10 +00:00
|
|
|
|
if (!isActiveAndEnabled) return;
|
|
|
|
|
|
StartCoroutine(DeathRoutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator DeathRoutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
selectRing.enabled = false;
|
|
|
|
|
|
highlightRing.enabled = false;
|
2026-11-03 19:29:16 +00:00
|
|
|
|
overwatchArea.gameObject.SetActive(false);
|
2026-11-04 04:05:46 +00:00
|
|
|
|
if (anim != null) anim.Play(ClipName("damage"), false, 99f, false);
|
2026-11-03 19:10:10 +00:00
|
|
|
|
float t = 0f;
|
|
|
|
|
|
const float dur = 0.45f;
|
|
|
|
|
|
Vector3 s0 = transform.localScale;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
var sr = anim != null ? anim.Renderer : body;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
while (t < dur)
|
|
|
|
|
|
{
|
|
|
|
|
|
t += Time.deltaTime;
|
|
|
|
|
|
float k = Mathf.Clamp01(t / dur);
|
|
|
|
|
|
transform.localScale = s0 * (1f - 0.5f * k);
|
2026-11-04 04:05:46 +00:00
|
|
|
|
var c = sr.color;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
c.a = 1f - k;
|
2026-11-04 04:05:46 +00:00
|
|
|
|
sr.color = c;
|
2026-11-03 19:10:10 +00:00
|
|
|
|
yield return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
unit.gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|