ThreeKingdomsSlingshot/Assets/Scripts/OutGame/Domestic/UiDrillAnim.cs

250 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 훈련 장면 병사 동작(내정 · PD 2026-11-12 「직군별 병사(범용 유닛)가 훈련 중인 장면을 애니메이션화」) — 원화 3 클립을 그대로 이어 붙임(새로 그리는 애니 없음 · 통일성):
/// 대기 숨 고르기(front_idle · 깜빡임) → 자세(front_ready) → 모으기(같은 그림 · 뒤로 당김) → 타격(front_action · 앞으로 내딛음 + 흙먼지) → 유지 → 대기.
/// 박자 = 표 domestic_scene.csv train_drill(전투 공격과 같은 「서기 0.5 · 모으기 0.3 · 타격 0.1 · 유지 0.5」 · PD 2026-11-01) · 모든 병사가 한 구령(같은 시계)에 · 뒷줄은 f_phase 만큼 늦게.
/// 매 프레임 바뀌므로 Frame 캔버스(장면 층) 안에서만.
/// </summary>
public class UiDrillAnim : MonoBehaviour
{
const string Idle = "front_idle";
const string Ready = "front_ready";
const string Action = "front_action";
const string DrillReady = "drill_ready";
const string DrillAction = "drill_action";
Image img;
RectTransform rt;
UnitArtData data;
Sprite[] idle;
Sprite readySp;
Sprite actionSp;
LobbyScene.Row drill;
LobbyScene.Row dustRow;
float delay;
float k;
float artW;
float idleOffset;
float nextBlink;
float blinkEnd = -1f;
Image dust;
Vector2 dustBase;
float lastHit = -99f;
int lastCycle = -1;
float readyDx;
float actionDx;
float readyScale = 1f;
float actionScale = 1f;
Vector3 baseScale = Vector3.one;
Vector2 dustPos;
Image shotImg;
LobbyScene.Row shotRow;
Vector2 shotPos;
float shotPx;
/// <summary>현재 단계(테스트 · 캡처 로그): 0 대기 · 1 자세 · 2 모으기 · 3 타격 · 4 유지.</summary>
public int Stage { get; private set; }
/// <summary>
/// feet = 표 train_feet 줄(그 병종 그림 · f_x = 자세 · f_w = 타격 오른쪽 이동 그림 px) — 모든 자세의 뒷발이 대기 때 뒷발 자리에 오게 그림을 옮김
/// (PD 2026-11-12 「위치는 고정한 채 발 위치와 모션만 바뀌어 부자연스러움」 → 몸이 내디딘 발을 따라 앞으로 · 그림을 바꿀 때 뒷발은 그대로라 미끄럼 없음). null = 옮기지 않음.
/// </summary>
public static UiDrillAnim Attach(Image img, UnitArtData data, LobbyScene.Row drill, float delay, float scale, float artW, Image dust, LobbyScene.Row dustRow, LobbyScene.Row feet)
{
Sprite[] id, rd, ac;
if (img == null || data == null || drill == null) return null;
if (!data.Clips.TryGetValue(Idle, out id) || id.Length == 0) return null;
var a = img.gameObject.AddComponent<UiDrillAnim>();
a.img = img;
a.rt = img.rectTransform;
a.data = data;
a.idle = id;
// 훈련 전용 클립(drill_ready · drill_action)이 있으면 그것(궁병 = 대기 원화 리그로 다시 만든 2 장 · PD 2026-11-12 「전혀 연결되지 않아. 다시 제작」) · 없으면 전투 자세·타격
a.readySp = Pick(data, DrillReady, Ready, id[0]);
a.actionSp = Pick(data, DrillAction, Action, a.readySp);
a.baseScale = img.rectTransform.localScale;
a.drill = drill;
a.delay = delay;
a.k = scale;
a.artW = artW;
a.dust = dust;
a.dustRow = dustRow;
if (dust != null)
{
a.dustBase = dust.rectTransform.sizeDelta;
a.dustPos = dust.rectTransform.anchoredPosition;
dust.enabled = false;
}
if (feet != null)
{
a.readyDx = feet.x;
a.actionDx = feet.w;
// 자세·타격 화면 배율(f_min · f_max · 기본 1) = 그림 크기가 대기와 다른 병종(책사 · PD 2026-11-12 「크기가 갑자기 바뀌어서 어색」) · 그림 파일은 그대로 · 발(피벗) 기준
a.readyScale = feet.min > 0.01f ? feet.min : 1f;
a.actionScale = feet.max > 0.01f ? feet.max : 1f;
}
// 숨 고르기는 병사마다 다르게(같이 숨 쉬면 어색) · 동작은 한 구령
a.idleOffset = Random.value * data.CycleSeconds(Idle, id.Length);
UiLayer.Isolate(img.gameObject);
return a;
}
static Sprite Pick(UnitArtData data, string first, string second, Sprite fallback)
{
Sprite[] f;
if (data.Clips.TryGetValue(first, out f) && f.Length > 0) return f[0];
if (data.Clips.TryGetValue(second, out f) && f.Length > 0) return f[0];
return fallback;
}
/// <summary>
/// 타격 순간 날아가는 화살(궁병 · 표 train_shot) — 병사 그림의 자식(같은 Frame 캔버스 · 같이 움직임) · 자세 그림에 메긴 화살 자리에서 시작해 f_speed 각도로 f_amp 그림 px 날아가며 작아지고 흐려짐.
/// shot: s_sprite = 「병사 아트 id|화살 테마 아이콘」 · f_x,f_y = 화살 가운데(그림 px · 왼쪽 위 0,0) · f_w = 길이 · f_scale = 굵기 배율 · f_amp = 거리 · f_period = 나는 시간(초) · f_speed = 각도(도 · 위 +).
/// </summary>
public void AttachShot(Sprite arrow, LobbyScene.Row shot)
{
if (arrow == null || shot == null || data == null) return;
float fw = data.Meta.frame != null && data.Meta.frame.Length > 1 ? data.Meta.frame[0] : idle[0].rect.width;
float fh = data.Meta.frame != null && data.Meta.frame.Length > 1 ? data.Meta.frame[1] : idle[0].rect.height;
float px = fw > 0f ? rt.rect.width / fw : 0f;
var s = UiKit.Rect("Arrow", rt, Vector2.zero, new Vector2(0.5f, 0.5f), new Vector2(shot.x * px, (fh - shot.y) * px),
new Vector2(shot.w * px, shot.w * px * arrow.rect.height / Mathf.Max(1f, arrow.rect.width) * (shot.scale > 0.01f ? shot.scale : 1f)));
s.localEulerAngles = new Vector3(0f, 0f, shot.speed);
shotImg = s.gameObject.AddComponent<Image>();
shotImg.sprite = arrow;
shotImg.raycastTarget = false;
shotImg.enabled = false;
shotRow = shot;
shotPos = s.anchoredPosition;
shotPx = px;
}
/// <summary>화살이 지금 날고 있는지(테스트 · 캡처 로그).</summary>
public bool ShotFlying
{
get { return shotImg != null && shotImg.enabled; }
}
float Cycle
{
get { return Mathf.Max(0.1f, drill.rest + drill.stand + drill.ready + drill.hit + drill.hold); }
}
static float Smooth(float x)
{
x = Mathf.Clamp01(x);
return x * x * (3f - 2f * x);
}
void Update()
{
if (img == null || rt == null || drill == null || data == null || idle == null || idle.Length == 0) return; // Play 중 재컴파일(스크립트 다시 불러옴) = 직렬화 안 된 값이 비면 멈춤
float now = Time.unscaledTime;
var area = rt.parent as RectTransform;
float unit = area != null && artW > 0f ? area.rect.width / artW : 1f;
float local = now - delay;
int cyc = Mathf.FloorToInt(local / Cycle);
float t = local - cyc * Cycle;
float t1 = drill.rest, t2 = t1 + drill.stand, t3 = t2 + drill.ready, t4 = t3 + drill.hit;
float back = drill.min * k, lunge = drill.amp * k; // 그림 px(병사 크기 배율 곱) · 표 0 = 끔(뒷발 맞춤으로 대신)
// 그림 px(원본 틀 = meta frame · Android 512 임포트와 무관) → 이 그래픽 단위
float fw = data.Meta.frame != null && data.Meta.frame.Length > 0 && data.Meta.frame[0] > 0 ? data.Meta.frame[0] : idle[0].rect.width;
float px = fw > 0f ? rt.rect.width / fw : 0f;
float off; // + = 위로(뒤로)
float dx; // + = 오른쪽(뒷발 맞춤)
Sprite sp;
if (t < t1)
{
Stage = 0;
float it = now + idleOffset;
int f = data.ApplyBlink(Idle, idle.Length, data.FrameAt(Idle, idle.Length, it), it, ref nextBlink, ref blinkEnd);
sp = idle[Mathf.Clamp(f, 0, idle.Length - 1)];
off = lunge > 0f ? -lunge * (1f - Smooth(t / Mathf.Max(0.01f, drill.period))) : 0f;
dx = 0f;
}
else if (t < t2)
{
Stage = 1;
sp = readySp;
off = 0f;
dx = readyDx;
}
else if (t < t3)
{
Stage = 2;
sp = readySp;
off = back * Smooth((t - t2) / Mathf.Max(0.01f, drill.ready));
dx = readyDx;
}
else if (t < t4)
{
Stage = 3;
sp = actionSp;
off = Mathf.Lerp(back, -lunge, Smooth((t - t3) / Mathf.Max(0.01f, drill.hit)));
dx = actionDx;
if (cyc != lastCycle)
{
lastCycle = cyc;
lastHit = now;
}
}
else
{
Stage = 4;
sp = actionSp;
off = -lunge;
dx = actionDx;
}
if (img.sprite != sp) img.sprite = sp;
var pos = new Vector2(Mathf.Round(dx * px), Mathf.Round(off * unit)); // 정수 화면 px(비정수 1~2 px 계단 방지 · R21 함정)
if (rt.anchoredPosition != pos) rt.anchoredPosition = pos;
float sc = Stage == 1 || Stage == 2 ? readyScale : Stage >= 3 ? actionScale : 1f;
var ls = new Vector3(baseScale.x * sc, baseScale.y * sc, baseScale.z);
if (rt.localScale != ls) rt.localScale = ls;
UpdateDust(now, unit, actionDx * px);
UpdateShot(now);
}
void UpdateShot(float now)
{
if (shotImg == null || shotRow == null) return;
float life = Mathf.Max(0.05f, shotRow.period);
float u = (now - lastHit) / life;
bool on = u >= 0f && u < 1f;
if (shotImg.enabled != on) shotImg.enabled = on;
if (!on) return;
float e = 1f - (1f - u) * (1f - u); // 빠르게 나가다 늦춤
float r = shotRow.speed * Mathf.Deg2Rad;
var srt = shotImg.rectTransform;
srt.anchoredPosition = shotPos + new Vector2(Mathf.Cos(r), Mathf.Sin(r)) * (shotRow.amp * shotPx * e);
float k = Mathf.Lerp(1f, 0.7f, e); // 멀어지며 작아짐
srt.localScale = new Vector3(k, k, 1f);
var c = shotImg.color;
c.a = u < 0.6f ? 1f : 1f - (u - 0.6f) / 0.4f;
shotImg.color = c;
}
void UpdateDust(float now, float unit, float hitDx)
{
if (dust == null || dustRow == null) return;
float life = Mathf.Max(0.05f, dustRow.period);
float u = (now - lastHit) / life;
bool on = u >= 0f && u < 1f;
if (dust.enabled != on) dust.enabled = on;
if (!on) return;
var dp = dustPos + new Vector2(Mathf.Round(hitDx), 0f); // 흙먼지 = 내디딘 몸 아래
if (dust.rectTransform.anchoredPosition != dp) dust.rectTransform.anchoredPosition = dp;
float grow = Mathf.Lerp(1f, Mathf.Max(1f, dustRow.max), Smooth(u));
var size = dustBase * grow;
if (dust.rectTransform.sizeDelta != size) dust.rectTransform.sizeDelta = size;
var c = dustRow.color;
c.a *= 1f - u;
dust.color = c;
}
}
}