// ─────────────────────────────────────────────────────────────────────────────
// SlashArcFollower.cs — 슬래시 궤적 이펙트가 플레이어를 따라가게 한다 (PD 지시 #777)
//
// PD 원문 (2026-09-06):
// "무기 궤적 이펙트가 플레이어 움직임에 맞게 위치가 자연스럽게 이동되어야하는데
// 지금은 고정 된 위치에 나와서 이펙트와 무기가 떨어진 느낌이야. 수정해줘"
//
// ── 왜 떨어져 보이는가 (구조) ───────────────────────────────────────────────
// #761 BurstMode.PrefabArc 는 스윙이 끝난 순간(OnSwingEnd) 검끝 궤적으로 평면을 구해
// 원본 슬래시 프리팹을 **월드 고정**으로 1회 배치한다(부모 없음). 배치 뒤에는 아무도
// 그 인스턴스를 움직이지 않는다.
// 거기에 #762 AttackStepDriver 가 공격마다 캐릭터를 0.30/0.70/0.65 m 전진시키므로
// (0.25 s 동안 NavMeshAgent.Move) **이펙트는 스윙 지점에 남고 캐릭터만 앞으로 나간다.**
// → 이펙트 수명(0.4~1.0 s) 동안 그 거리만큼 무기와 이펙트가 벌어진다.
//
// ── 이 컴포넌트가 하는 일 ───────────────────────────────────────────────────
// 스폰 콜백에서 인스턴스에 붙어, 매 LateUpdate 마다 **스폰 시점 PC 기준 상대 오프셋**을
// 유지하도록 위치를 갱신한다. 회전 추종은 선택(arcFollowRotation).
// · 위치만 추종 : pos = anchor.position + (스폰 시점 월드 오프셋)
// · 회전까지 추종: pos/rot = anchor 로컬 좌표계에 고정 (PC 가 돌면 이펙트도 돈다)
//
// ── 왜 부모로 붙이지 않는가 (설계 결정) ─────────────────────────────────────
// InGameInfo 는 이펙트 인스턴스를 풀(dic_str_Effect)에서 재사용하고 TurnOff_GO 가
// 수명이 끝나면 SetActive(false) 로 돌려놓는다. 이때 부모가 PC 로 바뀌어 있으면
// · PC 가 파괴/비활성될 때 풀 인스턴스가 같이 사라지거나 비활성 트리에 갇히고
// · 다음 재사용 때 부모가 남아 있어 엉뚱한 곳에서 재생된다.
// 풀 구조를 건드리지 않는 것이 조건이므로(발주 A2) **부모는 그대로 두고 좌표만 따라간다**.
// 기각안: transform.SetParent(pc) — 위 이유로 기각. 원복도 어렵다.
//
// ── 파티클 시뮬레이션 공간 ──────────────────────────────────────────────────
// 이미터가 Local(moveWithTransform=0)이면 이미 방출된 입자도 루트를 따라오므로 잔상이 없다.
// World 인 이미터가 섞여 있으면 그 입자만 제자리에 남는다 → 실측해 보고한다(발주 A2).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat
{
///
/// 스폰된 슬래시 이펙트 인스턴스가 플레이어를 따라 움직이게 한다.
/// 런타임에만 AddComponent 로 붙으므로 프리팹 에셋은 건드리지 않는다(즉시 원복 · C8).
///
[DisallowMultipleComponent]
public sealed class SlashArcFollower : MonoBehaviour
{
private Transform _anchor;
private bool _followRotation;
private float _expireTime; // Time.time 기준. 0 이하 = 이펙트 수명 전체
private bool _active;
private Vector3 _worldOffset; // 위치 추종 — 스폰 시점 (이펙트 − PC) 월드 오프셋
private Vector3 _localPos; // 회전 추종 — PC 로컬 위치
private Quaternion _localRot; // 회전 추종 — PC 로컬 회전
// ── 검증용 (Play 중 CLI 로 읽는다 · 게임 로직은 쓰지 않는다) ────────────
/// 지금까지 추종을 시작한 횟수.
public static int AttachCount;
/// 마지막으로 추종을 시작한 인스턴스.
public static SlashArcFollower Last;
/// 마지막 추종 시작 요약.
public static string LastInfo;
/// 추종 중인가.
public bool IsFollowing { get { return _active && _anchor != null; } }
/// 스폰 시점 (이펙트 − PC) 월드 오프셋.
public Vector3 SpawnOffset { get { return _worldOffset; } }
/// 추종 기준(PC) 트랜스폼. 해제됐으면 null.
public Transform Anchor { get { return _anchor; } }
///
/// 인스턴스에 추종을 건다. 이미 붙어 있으면 기준을 다시 잡는다(풀 재사용 대응).
///
/// 스폰된 이펙트 인스턴스
/// 따라갈 기준(PC 루트 트랜스폼)
/// PC 회전까지 따라갈지
/// 추종 유지 시간(초). 0 이하 = 이펙트 수명 전체
public static SlashArcFollower Attach(GameObject go, Transform anchor, bool followRotation, float seconds)
{
if (go == null || anchor == null) return null;
var f = go.GetComponent();
if (f == null) f = go.AddComponent();
f.Begin(anchor, followRotation, seconds);
return f;
}
/// 추종을 끈다(설정에서 arcFollowPlayer 를 껐을 때 · 풀 인스턴스 원복).
public static void Detach(GameObject go)
{
if (go == null) return;
var f = go.GetComponent();
if (f != null) f.Release();
}
private void Begin(Transform anchor, bool followRotation, float seconds)
{
_anchor = anchor;
_followRotation = followRotation;
_expireTime = seconds > 0f ? Time.time + seconds : 0f;
_active = true;
_worldOffset = transform.position - anchor.position;
_localPos = anchor.InverseTransformPoint(transform.position);
_localRot = Quaternion.Inverse(anchor.rotation) * transform.rotation;
AttachCount++;
Last = this;
LastInfo = string.Format("{0} anchor={1} offset={2} rot={3} sec={4:F2}",
gameObject.name, anchor.name, _worldOffset.ToString("F2"),
followRotation, seconds);
}
/// 추종을 즉시 해제한다. 위치는 그 자리에 남는다(기존 동작과 같아진다).
public void Release()
{
_active = false;
_anchor = null;
}
private void LateUpdate()
{
if (!_active) return;
if (_anchor == null) { _active = false; return; }
if (_expireTime > 0f && Time.time >= _expireTime) { _active = false; return; }
if (_followRotation)
{
transform.position = _anchor.TransformPoint(_localPos);
transform.rotation = _anchor.rotation * _localRot;
}
else
{
transform.position = _anchor.position + _worldOffset;
}
}
// 풀에 반환될 때(SetActive(false)) 반드시 끊는다 — 다음 재사용에서 이전 PC 를
// 계속 따라다니는 일이 없도록. 다시 쓸 때는 스폰 콜백이 Attach 로 새로 건다.
private void OnDisable() { Release(); }
}
}