205 lines
15 KiB
C#
205 lines
15 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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 string note = "";
|
||
}
|
||
|
||
public static class WLStaffLaser
|
||
{
|
||
public const string ResourcesPath = "WL/WLStaffLaser";
|
||
public static int Takes, Ticks, Hits, Pushes, Cancels, Turns, Reasserts, Ends; 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<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;
|
||
}
|
||
}
|
||
|
||
/// <summary>원본 Skill_Blizzard.Set 훅 — 규칙에 맞으면 레이저로 넘겨받는다(true = 원본 진행 건너뜀).</summary>
|
||
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<WLStaffLaserRunner>(); }
|
||
s_runner.Begin(pb, pd, pc, c); Takes++;
|
||
return true;
|
||
}
|
||
catch (System.Exception ex) { LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
|
||
}
|
||
|
||
/// <summary>레이저 중 그 방향으로 몸만 돌린다(러너가 가상패드 축을 읽어 부름).</summary>
|
||
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<Renderer>(); foreach (var x in pb.GetComponentsInChildren<Renderer>(true)) if (x.enabled) { x.enabled = false; ren.Add(x); }
|
||
var col = new List<Collider>(); foreach (var x in pb.GetComponentsInChildren<Collider>(true)) if (x.enabled) { x.enabled = false; col.Add(x); }
|
||
bool wasEnabled = pb.enabled; pb.enabled = false;
|
||
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<GameObject>(c.fx) : null;
|
||
if (p != null)
|
||
{
|
||
fx = Instantiate(p);
|
||
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(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<ParticleSystem>(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;
|
||
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<global::Joystick>(); 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<Collider>(); 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<UnityEngine.AI.NavMeshAgent>();
|
||
if (ag != null && ag.enabled && ag.isOnNavMesh) ag.Move(d); else a.transform.position += d;
|
||
WLStaffLaser.Pushes++;
|
||
}
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
}
|
||
}
|
||
}
|