WL #956 PD 지시 2건(비수) — ① 3타 몸 회전 1회 + 이펙트 동반 회전: 범인 = m_animation.transform 이 PC 루트 자체(Animator 가 루트)라 #955 「모델 자식 회전」도 루트 회전 → 원본 LookAtTarget 이 매 프레임 되돌려 클립당 61°(실측) → WLSwingExtras 가 엉덩이 뼈(Root_M)를 루트 축으로 누적 각 회전(LateUpdate · 루트·판정·카메라 그대로) + PC 에 붙은 WL_Impact_DaggerWhirl 같은 각 회전(spinFx) ② 주력 이펙트 = 정면 1.2 m 1회 사본 끔(fxMain 0) → 난사 루프 0.28 s 마다 양손 소켓(w_bladedancer_l/r · SocketTipOf) 손잡이→날 끝 0.6 자리에 작은 사본(0.35 · 0.6 s · 날 방향 추종 WLShadowSlashFxFollow) · Play 실측 시전당 7개 · 게임 오류 0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f2054e17d7
commit
b318bc0243
|
|
@ -122,6 +122,8 @@
|
|||
"lockFacing": false,
|
||||
"spinDegrees": 360,
|
||||
"spinSeconds": 0.9,
|
||||
"spinFx": true,
|
||||
"_note_956": "#956 PD 「3타 때 몸을 회전하지 않는 것 같다 → 몸을 회전 1회하며 공격 · 이펙트도 회전 모션에 맞게」 — Play 실측: m_animation.transform 은 PC 루트 자체(Animator 가 루트)라 #955 2차 「모델 자식 회전」도 루트 회전이었고 원본 LookAtTarget 이 매 프레임 되돌려 Attack3 클립당 61° 만 남음 → 코드 회전을 엉덩이 뼈(Root_M)에 누적 각으로 적용(루트·판정·카메라 그대로) + PC 에 붙은 이 이펙트를 같은 각으로(spinFx)",
|
||||
"_note_955b": "Play 실측: 팔라딘 Around 클립은 모델 yaw 36° 만 돌아(회전이 포즈에 안 구워짐) → 코드 회전 360°/0.9 s(클립 1.33 s ÷ 1.3)",
|
||||
"_note_955": "#955 PD 「3타 이펙트 범위가 단검 리치보다 과하고 회전 이펙트가 아님 → 실제로 한바퀴 회전하며 공격 + 회전하는 녹색 이펙트」 — 동작 = 매핑 3타를 Paladin skill_spinslash.FBX(Sword&Shield_Skill7_Around 1.33 s · 속도 1.3)로 · 이펙트 = 원본 Skill_SpinSlash(회전 칼날 고리 · 편집 모드 대조표 955_fx_sheet) 사본 WL_Impact_DaggerWhirl(스크립트·충돌체 제거 · 녹색 tint 재질 사본) PC 에 붙어 함께 돎 · 배율 1.0(실제 0.64 · 고리 반지름 ≈ 0.8 m 추정 = 단검 리치) · spinDegrees 0 = 클립 자체 회전(실측 뒤 안 돌면 360)",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ namespace WL.Combat.Impact
|
|||
public bool alignWeapon; public float alignFraction = 0.5f; public float[] massLocal;
|
||||
// #955 PD 비수 4타·3타 부가 연출 — blinkDistance: 띄우는 순간 PC 를 정면으로 순간이동(m · 잔상 blinkGhosts 개를 길에) · camHoldSeconds: 그동안 카메라 회전·추적 고정 + camZoomOut(m) 만큼 줌아웃했다 복귀 · spinDegrees/spinSeconds: PC 를 코드로 회전(클립에 회전이 없을 때 · 0 = 안 함)
|
||||
public float blinkDistance; public int blinkGhosts = 4; public float camHoldSeconds; public float camZoomOut; public float spinDegrees; public float spinSeconds = 0.6f;
|
||||
// #956 PD 「3타 때 몸을 회전 1회하며 공격 · 이펙트도 회전 모션에 맞게」 — spinFx: PC 에 붙은(follow) 이펙트를 몸과 같은 각으로 돌린다(기본 켬)
|
||||
public bool spinFx = true;
|
||||
// #943-⑤ PD 「타격하려는 방향을 고정한 채 이펙트가 재생되는 동안 타격 판정 … 이펙트가 캐릭터 방향에 따라 마구 바뀌면 안 됨」 — fixedDir = 띄우는 순간 PC 정면(LookAtTarget 뒤 = 표적 방향)을 잡아 두고 life 동안 이펙트 방향 고정 + PC 회전도 그 방향으로 잠금(공격 상태인 동안) · 자리 = PC 중심 + 정면 alignForward + 오른쪽 alignSide + 위 yOffset − R × massLocal × 배율(PC 위치는 따라감)
|
||||
public bool fixedDir; public float alignForward = 0.5f; public float alignSide; public bool lockFacing = true; }
|
||||
/// <summary>지면 충격파. tipContact = 칼끝이 지면(contactHeight 이내)에 닿는 프레임·자리에서 재생(단계 안에서 minDrop 이상 올라갔다 내려올 때) · 못 잡으면 timeout 뒤 최저점(fallbackHeight 이내)에.</summary>
|
||||
|
|
@ -655,8 +657,8 @@ namespace WL.Combat.Impact
|
|||
/// 띄우는 순간: 정면 blinkDistance 로 워프(NavMesh 위) + 길에 잔상(WLGhostStrike 살아 있는 자세) · camHoldSeconds 동안 카메라 회전·PC 기준 오프셋 고정 + sin 곡선 줌아웃 · spinDegrees 를 spinSeconds 에 걸쳐 회전(이펙트는 PC 에 붙어 함께 돈다).</summary>
|
||||
public sealed class WLSwingExtras : MonoBehaviour
|
||||
{
|
||||
PCActor _pc; float _spinLeft, _spinRate; bool _spinning;
|
||||
public static int Blinks, Spins, CamHolds; public static string LastLog = "";
|
||||
PCActor _pc; float _spinLeft, _spinRate, _spinAcc; bool _spinning; Transform _spinBone; GameObject _fx; Vector3 _fxPos; Quaternion _fxRot;
|
||||
public static int Blinks, Spins, CamHolds; public static string LastLog = "", SpinLog = "";
|
||||
public static void Begin(PCActor pc, WLSwingFxRule r, GameObject fx)
|
||||
{
|
||||
if (pc == null) return;
|
||||
|
|
@ -672,15 +674,36 @@ namespace WL.Combat.Impact
|
|||
Blinks++; LastLog = "blink " + Vector3.Distance(from, to).ToString("F2") + " m · 잔상 " + ghosts;
|
||||
}
|
||||
if (r.camHoldSeconds > 0f) { WLCamHold.Begin(pc, r.camHoldSeconds, r.camZoomOut); CamHolds++; }
|
||||
if (r.spinDegrees != 0f) { ex._spinLeft = r.spinDegrees; ex._spinRate = r.spinDegrees / Mathf.Max(0.05f, r.spinSeconds); ex._spinning = true; Spins++; }
|
||||
if (r.spinDegrees != 0f)
|
||||
{
|
||||
// #956 PD 「3타 때 몸을 회전 1회하며 공격 · 이펙트도 회전 모션에 맞게」 — m_animation.transform 은 PC 루트 자체(Animator 가 루트 LH_M05 · Play 실측)라
|
||||
// 「모델 자식 회전」(#955 2차)도 루트 회전이었고 원본 LookAtTarget 이 매 프레임 되돌려 클립당 61° 만 남았다 → 엉덩이 뼈(Humanoid Hips = Root_M)를 루트 축으로
|
||||
// 누적 각만큼 LateUpdate(애니메이션 평가 뒤)에서 돌린다 — 애니메이터가 뼈를 매 프레임 다시 쓰므로 델타가 아니라 누적 각 · 루트·판정 방향·카메라는 그대로(보이는 몸만) · PC 에 붙은 이펙트는 같은 각으로
|
||||
ex._spinBone = SpinBone(pc); ex._spinLeft = r.spinDegrees; ex._spinAcc = 0f; ex._spinRate = r.spinDegrees / Mathf.Max(0.05f, r.spinSeconds); ex._spinning = ex._spinBone != null;
|
||||
ex._fx = r.spinFx && fx != null && fx.transform.parent == pc.transform ? fx : null;
|
||||
if (ex._fx != null) { ex._fxPos = fx.transform.localPosition; ex._fxRot = fx.transform.localRotation; }
|
||||
Spins++; SpinLog = (ex._spinning ? "spin " + r.spinDegrees + "°/" + r.spinSeconds + " s · 뼈 " + ex._spinBone.name : "spin 뼈 없음") + (ex._fx != null ? " · fx 함께" : " · fx 없음");
|
||||
}
|
||||
}
|
||||
/// <summary>#956 돌릴 뼈 — Humanoid Hips(이 PC 는 루트 직계 Root_M) · 없으면 루트 직계 중 이름이 Root 로 시작하는 것.</summary>
|
||||
static Transform SpinBone(PCActor pc)
|
||||
{
|
||||
var an = pc.m_animation; Transform b = an != null && an.isHuman ? an.GetBoneTransform(HumanBodyBones.Hips) : null;
|
||||
if (b == null) foreach (Transform c in pc.transform) if (c.name.StartsWith("Root")) { b = c; break; }
|
||||
return b;
|
||||
}
|
||||
void LateUpdate()
|
||||
{
|
||||
if (!_spinning || _pc == null) return;
|
||||
var model = _pc.m_animation != null ? _pc.m_animation.transform : null; if (model == null) { _spinning = false; return; }
|
||||
if (_spinBone == null) { _spinning = false; _fx = null; return; }
|
||||
float step = _spinRate * Time.deltaTime; if (Mathf.Abs(step) > Mathf.Abs(_spinLeft)) step = _spinLeft;
|
||||
model.Rotate(0f, step, 0f, Space.Self); _spinLeft -= step; // 루트가 아니라 모델 자식을 돌린다(루트는 원본이 매 프레임 표적 쪽으로 되돌려 211°/2회 실측) · 끝에 로컬 yaw 원상
|
||||
if (Mathf.Approximately(_spinLeft, 0f)) { _spinning = false; var e = model.localEulerAngles; model.localEulerAngles = new Vector3(e.x, 0f, e.z); }
|
||||
_spinLeft -= step; _spinAcc += step;
|
||||
bool done = Mathf.Approximately(_spinLeft, 0f);
|
||||
float a = done ? 0f : _spinAcc; // 끝 = 애니메이터 포즈 그대로(원상) · 이펙트도 처음 자리
|
||||
var root = _pc.transform;
|
||||
if (a != 0f) _spinBone.RotateAround(root.position, root.up, a); // 루트 발 위치 · 위 축으로 몸 전체(뼈 아래 양손 무기 포함)를 돌린다
|
||||
if (_fx != null) { if (!_fx.activeInHierarchy || _fx.transform.parent != root) _fx = null; else { var q = Quaternion.Euler(0f, a, 0f); _fx.transform.localRotation = q * _fxRot; _fx.transform.localPosition = q * _fxPos; } }
|
||||
if (done) { _spinning = false; _fx = null; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -769,25 +792,33 @@ namespace WL.Combat.Impact
|
|||
var sockets = pc != null ? pc.tfs_weapon : null; if (sockets == null) return false;
|
||||
for (int i = 0; i < sockets.Length; i++)
|
||||
{
|
||||
var s = sockets[i]; if (s == null || !s.gameObject.activeInHierarchy) continue;
|
||||
foreach (var r in s.GetComponentsInChildren<Renderer>(false))
|
||||
{
|
||||
if (r == null || !r.enabled) continue;
|
||||
Bounds lb; Transform rt = r.transform;
|
||||
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
|
||||
if (mf != null && mf.sharedMesh != null) lb = mf.sharedMesh.bounds; else if (smr != null) lb = smr.localBounds; else continue;
|
||||
Vector3 far = Vector3.zero; float farD = -1f;
|
||||
for (int c = 0; c < 8; c++)
|
||||
{
|
||||
var corner = lb.center + new Vector3((c & 1) == 0 ? -lb.extents.x : lb.extents.x, (c & 2) == 0 ? -lb.extents.y : lb.extents.y, (c & 4) == 0 ? -lb.extents.z : lb.extents.z);
|
||||
var local = s.InverseTransformPoint(rt.TransformPoint(corner)); float d = local.magnitude;
|
||||
if (d > farD) { farD = d; far = local; }
|
||||
}
|
||||
if (farD > best) { best = farD; socket = s; tipLocal = far; }
|
||||
}
|
||||
Vector3 tl; float d = SocketTipOf(sockets[i], out tl);
|
||||
if (d > best) { best = d; socket = sockets[i]; tipLocal = tl; }
|
||||
}
|
||||
return socket != null && best > 0.05f;
|
||||
}
|
||||
/// <summary>#956 소켓 하나의 무기 끝(소켓 로컬) = 켜진 렌더러 bounds 모서리 중 소켓 원점에서 가장 먼 점 · 반환 = 그 거리(0 = 무기 없음). 비수처럼 양손 소켓을 따로 쓸 때(WLShadowSlash 난사 사본).</summary>
|
||||
internal static float SocketTipOf(Transform s, out Vector3 tipLocal)
|
||||
{
|
||||
tipLocal = Vector3.zero; float best = 0f;
|
||||
if (s == null || !s.gameObject.activeInHierarchy) return 0f;
|
||||
foreach (var r in s.GetComponentsInChildren<Renderer>(false))
|
||||
{
|
||||
if (r == null || !r.enabled) continue;
|
||||
Bounds lb; Transform rt = r.transform;
|
||||
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
|
||||
if (mf != null && mf.sharedMesh != null) lb = mf.sharedMesh.bounds; else if (smr != null) lb = smr.localBounds; else continue;
|
||||
Vector3 far = Vector3.zero; float farD = -1f;
|
||||
for (int c = 0; c < 8; c++)
|
||||
{
|
||||
var corner = lb.center + new Vector3((c & 1) == 0 ? -lb.extents.x : lb.extents.x, (c & 2) == 0 ? -lb.extents.y : lb.extents.y, (c & 4) == 0 ? -lb.extents.z : lb.extents.z);
|
||||
var local = s.InverseTransformPoint(rt.TransformPoint(corner)); float d = local.magnitude;
|
||||
if (d > farD) { farD = d; far = local; }
|
||||
}
|
||||
if (farD > best) { best = farD; tipLocal = far; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public void Watch(PCActor pc, WLGroundFxRule rule, int stage)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,14 +8,22 @@
|
|||
"fxPrefab": "WL/Fx/WL_Fx_DaggerMulti",
|
||||
"fxScale": 0.85,
|
||||
"fxDelay": 0.5,
|
||||
"fxForward": 1.2,
|
||||
"fxYOffset": 0.1,
|
||||
"fxForward": 0.45,
|
||||
"fxYOffset": 0.3,
|
||||
"fxLife": 2.5,
|
||||
"hideOriginalFx": 1,
|
||||
"slashStates": [ "attack", "DualBlades_Attack2" ],
|
||||
"slashInterval": 0.28,
|
||||
"slashSpeed": 1.8,
|
||||
"slashSeconds": 2.0,
|
||||
"fxMain": 0,
|
||||
"fxPerSlash": 1,
|
||||
"fxSlashScale": 0.35,
|
||||
"fxSlashLife": 0.6,
|
||||
"fxSlashFraction": 0.6,
|
||||
"fxSlashYOffset": 0.0,
|
||||
"fxSlashFollow": 1,
|
||||
"_note_956": "#956 PD 「주력 이펙트가 너무 먼 위치(정면 1.2 m)에 재생 → 플레이어가 휘두르는 무기 위치에 맞게(휘두르는 느낌)」 — 정면 1회 사본은 끔(fxMain 0 · 켜면 fxForward 0.45 · 손 높이 0.3) · 대신 난사 루프 0.28 s 마다 같은 프리팹의 작은 사본(0.35 · 편집 모드 실측 프리팹 단위 초승달 AABB 5.26 m → 약 1.8 m · 초승달 수명 0.30 s · 2번째 초승달 시차)을 양손 소켓(w_bladedancer_l/r) 손잡이→날 끝 0.6 자리에 번갈아 띄우고 0.6 s 동안 날 방향(수평)·위치를 따라가게",
|
||||
"_note_955": "#955 PD 「주력 쓸 때 캐릭터가 공중에 뜬 채 행동 → 제자리에서 단검 난사」 — 원본 상태 사슬 DualBlades_Jump_Start → JumpAttack1·2·3(공중 자세)을 대신해 2.0 s 동안 1·2타 상태를 0.28 s 마다 번갈아(배속 1.8 · 애니메이션 이벤트 끔 = 추가 타격·궤적 없음 · 이동은 원본대로 금지) · 2.1 s 뒤 원본 순간이동·Play_Idle 그대로",
|
||||
"_note_954": "#954 PD 「비수 주력 = Slash Effects FREE 의 녹색 난사(스크린샷)로」 — 사본 WL_Fx_DaggerMulti(= Multiple Slash 2 · 초승달 2개가 시차로 · 시작색 teal) · 원본 베기(go_effect · 판정 상자 그대로) 렌더러 끔 · 원본이 베기를 켜는 0.5 s 에 PC 정면 1.2 m · 높이 +0.1 · 배율 0.6 · 2.5 s 뒤 파괴 · 연기(smoke)는 그대로"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ namespace WL.Combat.Weapon
|
|||
// #955 PD 「주력 쓸 때 왜 캐릭터가 공중에 뜬 채 행동하나 — 제자리에서 단검을 난사하도록」 — 원본 상태 사슬(DualBlades_Jump_Start → JumpAttack1·2·3 = 공중 자세)을 쓰지 않고 slashSeconds 동안 slashStates 를 slashInterval 마다 번갈아 재생(배속 slashSpeed · 애니메이션 이벤트 끔 = 추가 타격·궤적 없음)
|
||||
public string[] slashStates = new string[0];
|
||||
public float slashInterval = 0.28f, slashSpeed = 1.8f, slashSeconds = 2.0f;
|
||||
// #956 PD 「주력 이펙트가 너무 먼 위치에 재생 → 플레이어가 휘두르는 무기 위치에 맞게(휘두르는 느낌)」 — fxMain 0 = 정면 1회 사본(fxForward)을 안 띄움 ·
|
||||
// fxPerSlash 1 = 난사 루프(slashInterval)마다 fxPrefab 의 작은 사본(fxSlashScale)을 무기가 달린 소켓(PCActor.tfs_weapon · 비수 = 양손 · 번갈아)의 손잡이→날 끝 fxSlashFraction 자리 + 위 fxSlashYOffset 에
|
||||
// 띄우고 fxSlashLife 동안 날 방향(수평 성분)·위치를 매 프레임 따라가게(fxSlashFollow 1 · 0 = 처음 자리 고정)
|
||||
public int fxMain = 1, fxPerSlash = 0, fxSlashFollow = 1;
|
||||
public float fxSlashScale = 0.35f, fxSlashLife = 0.6f, fxSlashFraction = 0.6f, fxSlashYOffset = 0f;
|
||||
}
|
||||
|
||||
public static class WLShadowSlash
|
||||
|
|
@ -88,7 +93,7 @@ namespace WL.Combat.Weapon
|
|||
if (!string.IsNullOrEmpty(c.fxPrefab) && !DSUtil.CheckNull(shooter)) // #954
|
||||
{
|
||||
if (c.hideOriginalFx != 0 && skill.go_effect != null) foreach (var rd in skill.go_effect.GetComponentsInChildren<Renderer>(true)) rd.enabled = false; // 풀 인스턴스라 한 번 끄면 유지
|
||||
Runner.StartCoroutine(CoFx(shooter, c));
|
||||
if (c.fxMain != 0) Runner.StartCoroutine(CoFx(shooter, c)); // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만
|
||||
}
|
||||
if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c)); // #955
|
||||
LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled;
|
||||
|
|
@ -126,6 +131,7 @@ namespace WL.Combat.Weapon
|
|||
string st = c.slashStates[i % c.slashStates.Length]; int h = Animator.StringToHash(st);
|
||||
if (an.HasState(0, h)) { an.CrossFadeInFixedTime(h, 0.05f, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLoops++; }
|
||||
else SlashLog = "상태 없음 " + st;
|
||||
if (c.fxPerSlash != 0 && !string.IsNullOrEmpty(c.fxPrefab)) SlashFx(shooter, c, i); // #956 휘두르는 손의 날 자리에 작은 사본
|
||||
i++;
|
||||
yield return new WaitForSeconds(Mathf.Max(0.05f, c.slashInterval));
|
||||
}
|
||||
|
|
@ -134,6 +140,28 @@ namespace WL.Combat.Weapon
|
|||
SlashLog = "난사 " + i + "회 · " + (Time.time - t0).ToString("F2") + " s";
|
||||
}
|
||||
|
||||
public static int FxSlashes; public static string FxSlashLog = "";
|
||||
static readonly System.Collections.Generic.List<Transform> s_sock = new System.Collections.Generic.List<Transform>(2);
|
||||
static readonly System.Collections.Generic.List<Vector3> s_tip = new System.Collections.Generic.List<Vector3>(2);
|
||||
/// <summary>#956 난사 1회마다 fxPrefab 의 작은 사본을 무기 날 자리에 — 소켓은 무기 메시가 달린 것만(비수 = w_bladedancer_l · _r · WLImpactRunner.SocketTipOf) · i 번째 난사는 소켓을 번갈아 · 추종은 WLShadowSlashFxFollow.</summary>
|
||||
static void SlashFx(Actor shooter, WLShadowSlashJson c, int i)
|
||||
{
|
||||
var pc = shooter as PCActor; if (pc == null || pc.tfs_weapon == null) { FxSlashLog = "PC 아님"; return; }
|
||||
if (i == 0 || s_sock.Count == 0)
|
||||
{
|
||||
s_sock.Clear(); s_tip.Clear();
|
||||
foreach (var s in pc.tfs_weapon) { Vector3 tl; if (WL.Combat.Impact.WLImpactRunner.SocketTipOf(s, out tl) > 0.05f) { s_sock.Add(s); s_tip.Add(tl); } }
|
||||
if (s_sock.Count == 0) { FxSlashLog = "무기 소켓 없음"; return; }
|
||||
}
|
||||
int k = i % s_sock.Count; var sock = s_sock[k]; if (sock == null) return;
|
||||
var p = Resources.Load<GameObject>(c.fxPrefab); if (p == null) { FxSlashLog = "fx 프리팹 없음 " + c.fxPrefab; return; }
|
||||
var go = Object.Instantiate(p); go.transform.localScale = p.transform.localScale * c.fxSlashScale;
|
||||
var f = go.AddComponent<WLShadowSlashFxFollow>(); f.Set(pc, sock, s_tip[k], c.fxSlashFraction, c.fxSlashYOffset, c.fxSlashFollow != 0); f.Apply();
|
||||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
|
||||
Object.Destroy(go, Mathf.Max(0.1f, c.fxSlashLife)); FxSlashes++;
|
||||
FxSlashLog = "난사 fx " + FxSlashes + " · " + sock.name + " 날 " + s_tip[k].magnitude.ToString("F2") + " m · @" + go.transform.position.ToString("F2") + " ×" + c.fxSlashScale;
|
||||
}
|
||||
|
||||
/// <summary>Actor 의 protected 켜기 함수(Model_OnOff · Weapon_OnOff(bool))를 true 로 부른다 — 원본 Co_Invisible 끝과 같은 호출(가상 함수는 PCActor 쪽이 불린다).</summary>
|
||||
static bool Call(ref MethodInfo m, string name, Actor a)
|
||||
{
|
||||
|
|
@ -171,4 +199,19 @@ namespace WL.Combat.Weapon
|
|||
public sealed class WLShadowSlashBase : MonoBehaviour { public Vector3 scale, size, center; public bool hasBox; }
|
||||
/// <summary>#954 코루틴 숙주(씬 밖 · 숨김).</summary>
|
||||
public sealed class WLShadowSlashRunner : MonoBehaviour { }
|
||||
/// <summary>#956 난사 사본 추종 — 자리 = 소켓(손잡이) → 날 끝 사이 fraction + 위 yOffset · 방향 = 날 방향의 수평 성분(없으면 PC 정면) · follow 면 매 프레임(LateUpdate · 애니메이션 평가 뒤) 다시 맞춘다.</summary>
|
||||
public sealed class WLShadowSlashFxFollow : MonoBehaviour
|
||||
{
|
||||
Actor _pc; Transform _sock; Vector3 _tip; float _frac, _yOff; bool _follow;
|
||||
public void Set(Actor pc, Transform sock, Vector3 tipLocal, float frac, float yOff, bool follow) { _pc = pc; _sock = sock; _tip = tipLocal; _frac = frac; _yOff = yOff; _follow = follow; }
|
||||
public void Apply()
|
||||
{
|
||||
if (_sock == null) { _follow = false; return; }
|
||||
Vector3 hilt = _sock.position, tip = _sock.TransformPoint(_tip), dir = tip - hilt; dir.y = 0f;
|
||||
if (dir.sqrMagnitude < 1e-4f) { dir = _pc != null ? _pc.transform.forward : Vector3.forward; dir.y = 0f; if (dir.sqrMagnitude < 1e-4f) dir = Vector3.forward; }
|
||||
transform.position = Vector3.Lerp(hilt, tip, _frac) + Vector3.up * _yOff;
|
||||
transform.rotation = Quaternion.LookRotation(dir.normalized, Vector3.up);
|
||||
}
|
||||
void LateUpdate() { if (_follow) Apply(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue