Project_WL/Assets/WL/Combat/Weapon/WLShadowSlash.cs

149 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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) 렌더러 끔
}
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; // 풀 인스턴스라 한 번 끄면 유지
Runner.StartCoroutine(CoFx(shooter, c));
}
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;
}
/// <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 { }
}