// ───────────────────────────────────────────────────────────────────────────── // WLStaffLaser.cs — 현자의 지팡이(10302) 기본 스킬: 전방 직선 레이저(화염 광선) 3 초 유지 · 0.5 초 간격 다단 피해 · 맞을 때마다 0.2 m 밀침 (#986 · 2026-10-26) // // PD 원문: 「지팡이 기본 스킬 시전 시 마법 시전 연출 후 지팡이 기본 공격 3타와 동일하게 손을 내지르는 모션과 함께 "RPG Game VFX Collection(URP)" 에셋의 // FX_Dragonfire 이펙트를 사용하게 해줘. 이 스킬은 전방 직선 방향으로 레이저 형태의 마법을 발사해 0.5초간격으로 다단히트 피해를 주며, 0.2m씩 적을 // 밀쳐낼 수 있으며 3초 동안 유지되고, 캐릭터가 방향을 변경할 경우 변경 된 방향으로 지속시간 동안 레이져 공격을 할 수 있어. (단, 구르기를 시도할 경우 마법이 즉시 취소)」 // ■ 그릇 = 원본 Skill_Blizzard(703304 · 시전 skill3 1 s · 지팡이 표 칸 3) — 원본 Set() 훅 1줄(Take)이 원본 눈보라 진행을 건너뛰고 넘겨받는다 // ■ 진행: 이동 막음 + 스킬 상태 → 다음 프레임 3타 자세(holdState · holdNormalized)로 애니메이터 멈춤(다른 상태로 바뀌면 다시 세움) → 손 앞(fxOffset)에 레이저(fx · 반복)를 // 시전자 정면으로 매 프레임 붙임 → interval 마다 정면 직선(length × width + 몸 반지름) 안 적에게 원본 투사체 피해(ProcessCollider) + 레이저 방향으로 push m 밀침 → seconds 뒤 끝 // ■ 방향 전환: 레이저 중엔 원본 CanMove() 가 막혀 조이스틱 이동(Run_byJoystick)이 안 불린다 → 러너가 매 프레임 가상패드 입력(섬 SimpleInput 축 · 테스트 씬 원본 FloatingJoystick · 편집기 방향키)을 // 카메라 정면 기준 방향으로 바꿔 몸만 turnDegPerSec 로 돌린다(이동 없음 · 레이저가 따라감) // ■ 구르기 = 즉시 취소(시전자 상태가 Dodge 가 되면 레이저를 끄고 애니메이터 · 이동을 되돌린다) // ■ 값 = Resources/WL/WLStaffLaser.json (C45) · enabled 0 = 원본 눈보라 그대로 // 🔴 Debug.LogError 금지(원본 팝업). // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using System.Collections.Generic; using UnityEngine; namespace WL.Combat.Weapon { [System.Serializable] public class WLStaffLaserJson { public int enabled = 0; public string skill = "Skill_Blizzard"; // 넘겨받을 원본 스킬 클래스 이름 public int classId = 10302; // 시전자 직업(0 = 모두) public string fx = "WL/Fx/WL_Fx_StaffLaser"; // 레이저 이펙트(Resources · 원본 빔 축 = 로컬 -Y) public float fxScale = 0.24f; public float[] fxOffset = { 0f, 0.55f, 0.45f }; // 시전자 정면 기준 (오른쪽, 위, 앞) m — 내민 손 앞 public float[] fxEuler = { -90f, 0f, 0f }; // 시전자 정면 기준 회전(빔 -Y → 정면 +Z) public float seconds = 3f, interval = 0.5f; // 유지 · 다단 간격(s) — 첫 피해는 레이저가 켜지는 순간 public float length = 8f, width = 1.2f; // 피해 직선(m · 시전자 발에서 정면 · 폭 = 전체 · 적 몸 반지름 더함) public float push = 0.2f; // 맞을 때마다 레이저 방향 밀침(m) public string holdState = "attack3"; // 손 내지른 자세(애니메이터 상태 · 지팡이 3타) public float holdNormalized = 0.5f; public float turnDegPerSec = 360f; // 가상패드 방향으로 도는 속도 public float steerDeadzone = 0.2f; // 가상패드 축 크기가 이보다 작으면 돌지 않음 public float fadeSeconds = 0.4f; // 끝 · 취소 때 방출을 멈추고 지우기까지 public int noExtra = 0; // #994 1 = 원본 스킬 부가 효과(눈보라 = 감속 디버프) 없이 피해만(WLStaffEarth noExtra 와 같은 방식) public string note = ""; } public static class WLStaffLaser { public const string ResourcesPath = "WL/WLStaffLaser"; public static int Takes, Ticks, Hits, Pushes, Cancels, Turns, Reasserts, Ends, NoExtra; public static string LastLog = ""; static WLStaffLaserJson s_cfg; static WLStaffLaserRunner s_runner; internal static PCActor Active; internal static WLStaffLaserJson ActiveCfg; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; Active = null; ActiveCfg = null; } internal static WLStaffLaserJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLStaffLaserJson(); var ta = Resources.Load(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; } } /// 원본 Skill_Blizzard.Set 훅 — 규칙에 맞으면 레이저로 넘겨받는다(true = 원본 진행 건너뜀). public static bool Take(ProjectileBase pb) { try { var c = Cfg; if (c.enabled == 0 || pb == null || pb.GetType().Name != c.skill) return false; var pd = pb.Get_ProjectTileData(); var pc = pd != null ? pd.shooter as PCActor : null; if (pc == null) return false; if (c.classId > 0 && pc.Get_ID() != c.classId) return false; if (s_runner == null) { var go = new GameObject("[WL986] StaffLaser"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent(); } s_runner.Begin(pb, pd, pc, c); Takes++; return true; } catch (System.Exception ex) { LastLog = "넘겨받기 실패 — " + ex.Message; return false; } } /// 레이저 중 그 방향으로 몸만 돌린다(러너가 가상패드 축을 읽어 부름). public static bool Steer(PCActor pc, Vector3 dir) { if (Active == null || !ReferenceEquals(pc, Active) || ActiveCfg == null) return false; dir.y = 0f; if (dir.sqrMagnitude < 1e-4f) return true; var want = Quaternion.LookRotation(dir.normalized); pc.transform.rotation = Quaternion.RotateTowards(pc.transform.rotation, want, ActiveCfg.turnDegPerSec * Time.deltaTime); Turns++; return true; } } internal sealed class WLStaffLaserRunner : MonoBehaviour { public void Begin(ProjectileBase pb, ProjectileData pd, PCActor pc, WLStaffLaserJson c) { StopAllCoroutines(); StartCoroutine(Run(pb, pd, pc, c)); } static Vector3 V(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; } IEnumerator Run(ProjectileBase pb, ProjectileData pd, PCActor pc, WLStaffLaserJson c) { // 원본 겉모습 · 판정 상자 · Update 끔(끝나면 되돌림 · 풀 재사용) var ren = new List(); foreach (var x in pb.GetComponentsInChildren(true)) if (x.enabled) { x.enabled = false; ren.Add(x); } var col = new List(); foreach (var x in pb.GetComponentsInChildren(true)) if (x.enabled) { x.enabled = false; col.Add(x); } bool wasEnabled = pb.enabled; pb.enabled = false; if (c.noExtra != 0 && pd != null) { pd.e_SkillExtraType = eSkillExtraType.None; pd.m_skillTypeConfigTable = null; pd.SetSkillExtraType = false; WLStaffLaser.NoExtra++; } // #994 PD 「지팡이 기본 스킬에 명중 된 적에게 디버프 이펙트가 발생되지 않도록」 if (OptionInfo.Ins != null) OptionInfo.Ins.Set_PCMove(pc, false); pc.Set_CurAnim(eAnim.Skill); WLStaffLaser.Active = pc; WLStaffLaser.ActiveCfg = c; yield return null; // 원본 시전 코루틴이 같은 프레임에 다음 동작을 틀 수 있어 한 프레임 뒤에 자세를 세운다 var an = pc.m_animation; float sp0 = an != null && an.speed > 0f ? an.speed : 1f; Hold(an, c); GameObject fx = null; var p = !string.IsNullOrEmpty(c.fx) ? Resources.Load(c.fx) : null; if (p != null) { fx = Instantiate(p); foreach (var ps in fx.GetComponentsInChildren(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; m.loop = true; } fx.transform.localScale = Vector3.one * c.fxScale; Place(fx, pc, c); } else WLStaffLaser.LastLog = "레이저 이펙트 없음 " + c.fx; float t = 0f, next = 0f; bool canceled = false; int hits0 = WLStaffLaser.Hits; while (t < c.seconds) { if (pc == null || pc.IsDead()) { canceled = true; break; } if (pc.Get_CurAnim() == eAnim.Dodge) { canceled = true; WLStaffLaser.Cancels++; break; } // PD 「구르기를 시도할 경우 마법이 즉시 취소」 if (pc.Get_CurAnim() < eAnim.Skill) pc.Set_CurAnim(eAnim.Skill); if (an != null && (!an.GetCurrentAnimatorStateInfo(0).IsName(c.holdState) || an.IsInTransition(0))) { Hold(an, c); WLStaffLaser.Reasserts++; } else if (an != null && an.speed != 0f) an.speed = 0f; SteerInput(pc, c); if (fx != null) Place(fx, pc, c); if (t >= next) { next += Mathf.Max(0.05f, c.interval); WLStaffLaser.Ticks++; Tick(pb, pc, c); } yield return null; t += Time.deltaTime; } // 끝 · 취소 — 레이저 방출을 멈추고 흐려지게 · 애니메이터 · 이동 · 스킬 상태 되돌림 · 원본 투사체 끔 if (fx != null) { foreach (var ps in fx.GetComponentsInChildren(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); Destroy(fx, Mathf.Max(0.05f, c.fadeSeconds)); } if (an != null && an.speed == 0f) an.speed = sp0; WLStaffLaser.Active = null; WLStaffLaser.ActiveCfg = null; if (OptionInfo.Ins != null && pc != null) OptionInfo.Ins.Set_PCMove(pc, true); if (!canceled && pc != null && !pc.IsDead()) pc.Play_Idle(); if (pb != null) { pb.enabled = wasEnabled; WLStaffEarth.OffOriginal(pb); } foreach (var x in ren) if (x != null) x.enabled = true; foreach (var x in col) if (x != null) x.enabled = true; WLStaffLaser.Ends++; WLStaffLaser.LastLog = (canceled ? "취소 " : "끝 ") + t.ToString("F2") + " s · 다단 " + WLStaffLaser.Ticks + " · 이번 명중 " + (WLStaffLaser.Hits - hits0) + " · 밀침 " + WLStaffLaser.Pushes + " · 회전 입력 " + WLStaffLaser.Turns + " · 자세 다시 세움 " + WLStaffLaser.Reasserts; } // 가상패드 축(SimpleInput) → 카메라 정면 기준 수평 방향 → Steer(몸만 회전) static void SteerInput(PCActor pc, WLStaffLaserJson c) { var a = PadAxis(); float h = a.x, v = a.y; if (h * h + v * v < c.steerDeadzone * c.steerDeadzone) return; var cam = Camera.main; var fwd = cam != null ? cam.transform.forward : Vector3.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize(); WLStaffLaser.Steer(pc, fwd * v + Vector3.Cross(Vector3.up, fwd) * h); } // 가상패드 입력 — 섬 = SimpleInput 축(WLIslandAvatar) · 테스트 씬 = 원본 가상패드(Joystick Pack FloatingJoystick · global::Joystick.Direction) · 편집기 방향키 — 먼저 잡히는 것 static global::Joystick s_joy; static float s_joyFind; internal static Vector2 PadAxis() { var a = Vector2.zero; try { a = new Vector2(SimpleInput.GetAxis("Horizontal"), SimpleInput.GetAxis("Vertical")); } catch { } if (a.sqrMagnitude < 1e-4f) { if (s_joy == null && Time.unscaledTime >= s_joyFind) { s_joy = Object.FindAnyObjectByType(); s_joyFind = Time.unscaledTime + 1f; } if (s_joy != null && s_joy.isActiveAndEnabled) a = s_joy.Direction; } if (a.sqrMagnitude < 1e-4f) a = new Vector2((Input.GetKey(KeyCode.RightArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.LeftArrow) ? 1f : 0f), (Input.GetKey(KeyCode.UpArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.DownArrow) ? 1f : 0f)); return a; } static void Hold(Animator an, WLStaffLaserJson c) { if (an == null) return; an.speed = 0f; an.Play(c.holdState, 0, Mathf.Clamp01(c.holdNormalized)); an.Update(0f); } static void Place(GameObject fx, PCActor pc, WLStaffLaserJson c) { var f = pc.transform.forward; f.y = 0f; if (f.sqrMagnitude < 1e-4f) f = Vector3.forward; var basis = Quaternion.LookRotation(f.normalized); fx.transform.SetPositionAndRotation(pc.transform.position + basis * V(c.fxOffset), basis * Quaternion.Euler(V(c.fxEuler))); } // 정면 직선(시전자 발에서 length · 폭 width + 적 몸 반지름) 안 적 — 원본 투사체 피해 + 레이저 방향 밀침 static void Tick(ProjectileBase pb, PCActor pc, WLStaffLaserJson c) { var info = ActorInfo.Ins; if (info == null || pb == null) return; var o = pc.Get_position(); var f = pc.transform.forward; f.y = 0f; if (f.sqrMagnitude < 1e-4f) return; f.Normalize(); var list = info.Get_EnemyActors(pc.IsEnemy(), o, c.length + 3f); if (list == null) return; try { for (int k = 0; k < list.Count; k++) { var a = list[k]; if (a == null || a.IsDead()) continue; var v = a.Get_position() - o; v.y = 0f; float along = Vector3.Dot(v, f), side = Mathf.Abs(Vector3.Cross(f, v).y), br = WL.Combat.WLCombatMotionSettings.BodyRadius(a); if (along < -br || along > c.length + br || side > c.width * 0.5f + br) continue; var cl = a.GetComponent(); if (cl == null || !cl.enabled) continue; if (!pb.ProcessCollider(cl, false)) continue; WLStaffLaser.Hits++; if (c.push > 0f) { var d = f * c.push; var ag = a.GetComponent(); if (ag != null && ag.enabled && ag.isOnNavMesh) ag.Move(d); else a.transform.position += d; WLStaffLaser.Pushes++; } } } finally { info.Release_Actors(list); } } } }