218 lines
17 KiB
C#
218 lines
17 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLShadowSlash.cs — 단검(비수 1043 · Dagger 10202) 주력 「그림자 베기」(Skill_ShadowSlash) — 시전 중 캐릭터 보이기 · 이펙트 크기 배율
|
||
//
|
||
// PD 원문(#943-⑧ · 2026-10-19): 「단검 주력 스킬 시전 시 캐릭터의 모습이 보이도록 수정하고, 이펙트의 크기를 절반으로 줄여줘.」
|
||
//
|
||
// ■ 안 보이던 원인(코드 실측) — 원본 Skill_ShadowSlash.Set 이 shooter.Set_Invisible(2.1 초)를 부른다 → Actor.Co_Invisible 이
|
||
// 애니메이터 배속 0 · 무적 · Model_OnOff(false)(파티클 외 렌더러 전부 끔) · Weapon_OnOff(false) · 이동 금지 → 2.1 초 뒤 되돌린 다음 대상 뒤 1.5 m 로 순간이동.
|
||
// 이펙트가 덮거나 카메라 문제가 아니라 원본 스킬이 모델을 끈다.
|
||
// ■ 처리 — 원본 Set 의 Set_Invisible 뒤 훅 1줄(OnSet): Co_Invisible 이 끈 렌더러 · 무기를 바로 다시 켠다(원본이 2.1 초 뒤 부르는 같은 함수).
|
||
// freezeAnim 0 이면 애니메이터 배속도 1 로 되돌려 원본 상태 사슬 DualBlades_Jump_Start → JumpAttack1·2·3 → Jump_End → idle 이 보인다
|
||
// (JumpAttack1~3 클립 이벤트 0 개 · Jump_Start 의 SkillEvent 는 시각 0 하나 · 종료 전이 0.7 이라 다시 안 돈다 — 2026-10-19 컨트롤러 · FBX 메타 실측).
|
||
// 무적 · 이동 금지 · 2.1 초 뒤 대상 뒤 순간이동 · 판정은 원본 그대로.
|
||
// ■ 이펙트 — 베기(go_effect = Slash · 파티클 6 층 · 크기 2.6 ~ 7.7)와 연기(go_invisibleEffect = FX_DarkSkill_SmokeExplosion)의 Transform 을
|
||
// 「처음 크기 × 배율」로(풀 재사용 때 누적 없음 · 처음 크기는 WLShadowSlashBase 에 기억) · 파티클 크기 모드 Hierarchy
|
||
// (베기 팩 원본이 Local 이라 Transform 배율이 안 먹는다). 베기 판정 상자(BoxCollider 5.25 × 9 × 5.9)는 keepHitBox 1 이면 배율의 역수로 키워 월드 크기 그대로.
|
||
// ■ 값 = Resources/WL/WLShadowSlash.json (C45) · 끄기 = enabled 0(원본 그대로)
|
||
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업) — 실패는 LastLog
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
[System.Serializable]
|
||
public class WLShadowSlashJson
|
||
{
|
||
public int enabled = 0;
|
||
public int keepVisible = 1; // 1 = 시전 중 PC 렌더러 · 무기를 켠다(원본 = 2.1 초 숨김)
|
||
public int freezeAnim = 0; // 1 = 원본처럼 애니메이터 배속 0 유지(보이면 2.1 초 정지 자세) · 0 = 원본 상태 사슬 재생
|
||
public float slashScale = 1f; // 베기(go_effect) 보이는 크기 배율
|
||
public float smokeScale = 1f; // 연기(go_invisibleEffect) 크기 배율
|
||
public int keepHitBox = 1; // 1 = 베기 판정 상자 월드 크기 유지(보이는 크기만 바꿈)
|
||
// #954 PD 「비수 주력 = Slash Effects FREE 의 녹색 난사(Multiple Slash 2)로」 — 원본 베기 렌더러를 끄고(판정 상자 그대로) WL 사본을 같은 자리·시각에 띄운다
|
||
public string fxPrefab = ""; // Resources 경로(비우면 원본 베기 그대로)
|
||
public float fxScale = 1f, fxDelay = 0.5f, fxForward = 1.5f, fxYOffset = 0f, fxLife = 2.5f; // 배율 · 시전 뒤 시각(원본 go_effect 0.5 s) · PC 정면 거리(원본 1.5) · 높이 · 수명
|
||
public int hideOriginalFx = 0; // 1 = 원본 베기(go_effect) 렌더러 끔
|
||
// #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
|
||
{
|
||
public const string ResourcesPath = "WL/WLShadowSlash";
|
||
public static int Casts, Shown, Scaled, Errors;
|
||
public static string LastLog = "";
|
||
|
||
static WLShadowSlashJson s_cfg;
|
||
static MethodInfo s_model, s_weapon;
|
||
|
||
static WLShadowSlashJson Cfg
|
||
{
|
||
get
|
||
{
|
||
if (s_cfg != null) return s_cfg;
|
||
s_cfg = new WLShadowSlashJson();
|
||
var ta = Resources.Load<TextAsset>(ResourcesPath);
|
||
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
|
||
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
|
||
return s_cfg;
|
||
}
|
||
}
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; LastLog = ""; }
|
||
|
||
public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · " + LastLog; }
|
||
|
||
/// <summary>원본 Skill_ShadowSlash.Set 훅(Set_Invisible 바로 뒤 1줄) — 캐릭터 보이기 · 베기 · 연기 크기 배율.</summary>
|
||
public static void OnSet(Skill_ShadowSlash skill)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg;
|
||
if (skill == null || c.enabled == 0) return;
|
||
Casts++;
|
||
var pd = skill.Get_ProjectTileData();
|
||
var shooter = pd != null ? pd.shooter : null;
|
||
if (c.keepVisible != 0 && !DSUtil.CheckNull(shooter))
|
||
{
|
||
bool model = Call(ref s_model, "Model_OnOff", shooter);
|
||
bool weapon = Call(ref s_weapon, "Weapon_OnOff", shooter);
|
||
if (model && weapon) Shown++;
|
||
if (c.freezeAnim == 0 && shooter.m_animation != null) shooter.m_animation.speed = 1f;
|
||
}
|
||
if (skill.go_effect != null) Scale(skill.go_effect.transform, c.slashScale, c.keepHitBox != 0);
|
||
if (skill.go_invisibleEffect != null) Scale(skill.go_invisibleEffect.transform, c.smokeScale, false);
|
||
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; // 풀 인스턴스라 한 번 끄면 유지
|
||
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;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "실패 — " + ex.Message; }
|
||
}
|
||
|
||
public static int FxShown;
|
||
static WLShadowSlashRunner s_runner;
|
||
static WLShadowSlashRunner Runner { get { if (s_runner == null) { var go = new GameObject("[WL954] ShadowSlashFx"); go.hideFlags = HideFlags.HideAndDontSave; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLShadowSlashRunner>(); } return s_runner; } }
|
||
/// <summary>#954 원본 베기가 켜지는 시각(fxDelay)에 WL 사본(녹색 난사)을 PC 정면 fxForward 에 띄운다(PC 방향 · fxLife 뒤 파괴).</summary>
|
||
static System.Collections.IEnumerator CoFx(Actor shooter, WLShadowSlashJson c)
|
||
{
|
||
if (c.fxDelay > 0f) yield return new WaitForSeconds(c.fxDelay);
|
||
if (DSUtil.CheckNull(shooter) || shooter.IsDead()) yield break;
|
||
var p = Resources.Load<GameObject>(c.fxPrefab); if (p == null) { LastLog = "fx 프리팹 없음 " + c.fxPrefab; yield break; }
|
||
var fwd = shooter.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize();
|
||
var pos = shooter.Get_CenterPositionFoward(c.fxForward) + Vector3.up * c.fxYOffset;
|
||
var go = Object.Instantiate(p, pos, Quaternion.LookRotation(fwd, Vector3.up)); go.transform.localScale = p.transform.localScale * c.fxScale;
|
||
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.5f, c.fxLife)); FxShown++; LastLog = "fx " + c.fxPrefab + " @" + pos.ToString("F2") + " ×" + c.fxScale;
|
||
}
|
||
|
||
public static int SlashLoops; public static string SlashLog = "";
|
||
/// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상.</summary>
|
||
static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c)
|
||
{
|
||
var an = shooter.m_animation; if (an == null) yield break;
|
||
bool prevEvents = an.fireEvents; an.fireEvents = false; float t0 = Time.time; int i = 0;
|
||
try
|
||
{
|
||
while (Time.time - t0 < c.slashSeconds)
|
||
{
|
||
if (DSUtil.CheckNull(shooter) || shooter.IsDead()) break; // CurAnim 조건은 뺀다(실측: 3회 만에 끊겨 원본 콤보 전이 Attack3→4(회전)·Jump 사슬이 이어졌다) · 0.28 s 마다 다시 시작하므로 상태 exit 전이에 안 걸린다
|
||
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));
|
||
}
|
||
}
|
||
finally { if (!DSUtil.CheckNull(shooter) && shooter.m_animation != null) { shooter.m_animation.fireEvents = prevEvents; shooter.m_animation.speed = 1f; } }
|
||
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)
|
||
{
|
||
if (m == null) m = typeof(Actor).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new[] { typeof(bool) }, null);
|
||
if (m == null) { LastLog = "함수 없음 " + name; return false; }
|
||
m.Invoke(a, new object[] { true });
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Transform 을 처음 크기 × k 로 · 파티클 크기 모드 Hierarchy · 판정 상자는 keepBox 면 1/k 로(월드 크기 유지).</summary>
|
||
static void Scale(Transform t, float k, bool keepBox)
|
||
{
|
||
if (t == null || k <= 0f) return;
|
||
var b = t.GetComponent<WLShadowSlashBase>();
|
||
if (b == null)
|
||
{
|
||
if (Mathf.Approximately(k, 1f)) return; // 손댄 적 없고 배율 1 = 원본 그대로
|
||
b = t.gameObject.AddComponent<WLShadowSlashBase>();
|
||
b.scale = t.localScale;
|
||
var bc0 = t.GetComponent<BoxCollider>();
|
||
if (bc0 != null) { b.hasBox = true; b.size = bc0.size; b.center = bc0.center; }
|
||
foreach (var ps in t.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
|
||
}
|
||
t.localScale = b.scale * k;
|
||
if (b.hasBox)
|
||
{
|
||
var bc = t.GetComponent<BoxCollider>();
|
||
if (bc != null) { float inv = keepBox ? 1f / k : 1f; bc.size = b.size * inv; bc.center = b.center * inv; }
|
||
}
|
||
Scaled++;
|
||
}
|
||
}
|
||
|
||
/// <summary>#943-⑧ 이 Transform(원본 스킬 풀 인스턴스)의 처음 크기 · 판정 상자 — 배율 누적 방지.</summary>
|
||
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(); }
|
||
}
|
||
}
|