364 lines
25 KiB
C#
364 lines
25 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLStaffEarth.cs — 지팡이(현자의 지팡이 · 클래스 10302) 주력 · 각성 · 3타를 대지(Stylized Earth Skills) WL 방식으로 (#935 · 2026-10-18)
|
||
//
|
||
// PD 원문(#935):
|
||
// ③ 「지팡이 주력 스킬 시전 시 캐릭터의 주문 시전 모션에 모으기 이펙트를 재생시키고, Casual RPG VFX 에셋의 첨부한 스크린샷과 같은 이펙트를 시전한 캐릭터몸에 맞게 발생시켜줘.」
|
||
// ④ 「지팡이 주력 스킬 시전 시 타겟이 된 대상에게 Stylized Earth Skills에셋의 "FX_EarthSkill_Summon_04" 이펙트를 재생시켜 공격하고
|
||
// 타겟이 된 대상 및 주변의 적에게도 광역 피해 및 스턴 효과를 1.5초간 부여해줘」
|
||
// ⑤ 「지팡이 각성 스킬 시전 시간도 0.5초로 줄이고, 주력 스킬과 동일하게 모으기 이펙트 및 캐릭터 주변 시전 이펙트를 적용해. 그리고 시전한 스킬을
|
||
// FX_Earthskill_Path_03_1을 캐릭터를 중심으로 시전시키켜 범위 내 명중 된 적을 2m 띄운 후 떨어질 때 2초간 기절 시키게 해줘.」
|
||
// ⑥ 「지팡이 스킬 3타에는 타겟이 된 대상에게 FX_EarthSkill_Popup_04 이펙트를 재생시켜줘. 이 공격에 맞은 적은 피해를 받을 때 1.5m 띄워졌다가 0.5초 기절해야 해.」
|
||
//
|
||
// ■ 시전 — CombatEvents.SkillCast(시전 시작)에 castSkills 의 스킬이면 무기 끝에 모으기(chargeFx) · 발밑에 몸 기둥(auraFx · 몸 크기 비례) — 둘 다 따라가고
|
||
// 시전 시간 + life 뒤 fade 초 동안 흐려진다
|
||
// ■ 주력 · 각성 — 원본 Set() 의 훅 1줄(TakeOver)이 원본 진행(소용돌이 15 초 · 운석 3.75 초)을 건너뛴다: center(대상 · 시전자) 발밑에 fx →
|
||
// hitDelay(+ 중심에서 거리 / spreadSpeed) 뒤 radius 안의 적에게 원본 투사체 ProcessCollider(스킬 피해 · 피격 반응) + WLLaunch(띄우기 · 기절) →
|
||
// endAfter 뒤 원본 투사체 끄기(풀). 진행 중엔 원본 겉모습 · 판정 상자 · Update 를 끈다(끈 뒤 되돌림)
|
||
// ■ 3타 — 투사체 Set 끝 훅(WLProjectileVisual.OnSet → OnProjectileSet)이 쏘는 순간 애니메이터 현재 상태가 popup.stateName(attack3)이면 그 투사체를 표시 →
|
||
// 그 투사체의 피해 확정(HitConfirmed)에 맞은 적 발밑에 popup fx + WLLaunch(띄우기 · 내려온 뒤 기절)
|
||
// ■ 값 = Resources/WL/WLStaffEarth.json (C45) · enabled 0 = 구독 0 · 훅은 false(원본 그대로)
|
||
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다) — 예외는 LastLog 로만
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using WL.Combat.Core;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
[System.Serializable]
|
||
public class WLStaffEarthFx
|
||
{
|
||
public string prefab = "";
|
||
public float scale = 1f;
|
||
public int bodyScale; // 1 = scale × 시전자 몸 크기 비례(WLImpactFx.SizeFactor)
|
||
public float[] offset = new float[3]; // 시전자 정면 기준 (오른쪽, 위, 앞) m
|
||
public float[] euler = new float[3];
|
||
public float life = 2f; // 남는 시간(초) — 시전 이펙트는 시전 시간 + life
|
||
public float fade = 0.4f; // 0 보다 크면 마지막 fade 초 동안 흐려지며 사라진다(WLFxFade)
|
||
public int follow; // 1 = 무기 끝 · 시전자를 따라간다
|
||
public string socket = ""; // 모으기 무기 소켓 이름(빈 값 = 무기가 달린 켜진 첫 소켓)
|
||
public int head; // #963 1 = 무기 메시 머리(로컬 경계 긴 축 먼 쪽 끝 − headPull · 끝 빛과 같은 점) · 0 = 이전(소켓 아래 렌더러 월드 상자 꼭대기 · 기울면 머리에서 벗어남)
|
||
public float headPull = 0.08f;
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthSkill
|
||
{
|
||
public string skill = ""; // 원본 스킬 클래스 이름(Skill_Hurricane · Skill_MeteoStorm)
|
||
public string center = "target"; // target = 대상 발밑 · caster = 시전자 발밑
|
||
public WLStaffEarthFx fx = new WLStaffEarthFx();
|
||
public float hitDelay = 0.4f; // fx 뒤 판정까지(초)
|
||
public float spreadSpeed; // 0 보다 크면 중심에서 거리 / 이 속도(m/s)만큼 판정이 늦어진다(퍼져 나가는 바위 줄기)
|
||
public float radius = 2.5f; // 판정 반경(m · 적 몸 반경을 더함)
|
||
public float launchHeight; // 띄우기 높이(m · 0 = 띄우지 않음)
|
||
public float launchSeconds = 0.6f; // 떠 있는 시간(초)
|
||
public float stunSeconds; // 내려온 뒤 기절(초)
|
||
public float endAfter = 1f; // 마지막 판정 뒤 원본 투사체를 끄기까지(초)
|
||
// #963 PD 「지팡이 각성 = Hyper Casual FX Pack Vol.2 메테오 — 타겟 대상 주위 5 m 에 운석 10개를 랜덤 낙하 · 낙하 자리의 적에게 피해 · 맞은 적 0.5 초 스턴」 —
|
||
// meteors > 0 = 메테오 방식: meteorInterval 마다 center 둘레 meteorArea(m) 원 안 무작위 자리에 fx(운석 1개 = 낙하 + 충돌)를 띄우고 meteorImpact 초 뒤(사본이 땅에 닿는 시각) 그 자리 radius 안 적에게 원본 투사체 피해 + 기절(stunSeconds) ·
|
||
// meteorYaw = 운석이 날아오는 쪽(카메라 수평 정면을 이 각만큼 돌린 방향 · 사본 로컬 +Z 위쪽 앞에서 떨어진다)
|
||
public int meteors;
|
||
public float meteorArea = 5f, meteorInterval = 0.12f, meteorImpact = 0.67f, meteorYaw = 30f;
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthPopup
|
||
{
|
||
public int classId = 10302;
|
||
public string stateName = "attack3"; // 쏘는 순간 애니메이터 현재 상태 이름(지팡이 콤보는 애니메이터 전환으로 이어져 CurAnim · ComboTracker 단계가 1 에 머문다)
|
||
public WLStaffEarthFx fx = new WLStaffEarthFx();
|
||
public float launchHeight = 1.5f, launchSeconds = 0.6f, stunSeconds = 0.5f;
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthJson
|
||
{
|
||
public int enabled = 0;
|
||
public string[] castSkills = new string[0];
|
||
public WLStaffEarthFx chargeFx = new WLStaffEarthFx();
|
||
public WLStaffEarthFx auraFx = new WLStaffEarthFx();
|
||
public WLStaffEarthSkill[] skills = new WLStaffEarthSkill[0];
|
||
public WLStaffEarthPopup popup = new WLStaffEarthPopup();
|
||
// #963 PD 「플레이어가 마법을 시전할 때는 Hyper Casual FX Pack Vol.1 의 마법진을 플레이어가 있는 바닥에 재생되었다가 시전(주황색 계열)」 — castCircleClasses 직업의 스킬 시전(SkillCast)마다 발밑에 castCircle(시전 시간 + life · 따라감)
|
||
public int[] castCircleClasses = new int[0];
|
||
public WLStaffEarthFx castCircle = new WLStaffEarthFx();
|
||
}
|
||
|
||
public static class WLStaffEarth
|
||
{
|
||
public const string ResourcesPath = "WL/WLStaffEarth";
|
||
public static int Casts, TakeOvers, Hits, Launches, Marked, Popups, Errors, Circles, Meteors;
|
||
public static string MeteorSrc = ""; public static Vector3 MeteorCenter; public static readonly List<Vector3> MeteorPts = new List<Vector3>(), MeteorCenters = new List<Vector3>(); // #964 검증용(메테오 중심 출처 · 낙하 자리)
|
||
public static string LastLog = "";
|
||
|
||
static WLStaffEarthJson s_cfg;
|
||
static readonly Dictionary<string, GameObject> s_prefabs = new Dictionary<string, GameObject>();
|
||
static readonly HashSet<int> s_marked = new HashSet<int>();
|
||
static WLStaffEarthRunner s_runner;
|
||
static MethodInfo s_off;
|
||
|
||
static WLStaffEarthJson Cfg
|
||
{
|
||
get
|
||
{
|
||
if (s_cfg != null) return s_cfg;
|
||
s_cfg = new WLStaffEarthJson();
|
||
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; s_prefabs.Clear(); s_marked.Clear();
|
||
CombatEvents.SkillCast.Remove(OnSkillCast); CombatEvents.HitConfirmed.Remove(OnHitConfirmed);
|
||
if (Cfg.enabled == 0) return;
|
||
CombatEvents.SkillCast.Add(OnSkillCast); CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||
}
|
||
|
||
// ── 시전: 모으기(무기 끝) · 몸 기둥(발밑) ──────────────────────────────────────────
|
||
static void OnSkillCast(in SkillCastEvent e)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; var pc = e.actor as PCActor;
|
||
if (pc == null || e.skill == null) return;
|
||
float cast = Mathf.Max(0f, e.skill.f_CastingTime);
|
||
if (pc.IsMainPC() && c.castCircleClasses != null && System.Array.IndexOf(c.castCircleClasses, pc.Get_ID()) >= 0 && Spawn(c.castCircle, pc, pc.transform, pc.Get_position(), cast) != null) Circles++; // #963 마법 시전 마법진
|
||
if (c.castSkills == null || System.Array.IndexOf(c.castSkills, e.skill.e_Skill.ToString()) < 0) return;
|
||
var sock = Socket(pc, c.chargeFx.socket);
|
||
if (Spawn(c.chargeFx, pc, sock, sock != null ? Tip(sock, c.chargeFx) : pc.Get_Center_position(), cast) != null) Casts++;
|
||
Spawn(c.auraFx, pc, pc.transform, pc.Get_position(), cast);
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "시전 이펙트 실패 — " + ex.Message; }
|
||
}
|
||
|
||
/// <summary>무기 소켓 — 이름이 맞는 켜진 소켓, 없으면 무기(자식)가 달린 켜진 첫 소켓.</summary>
|
||
static Transform Socket(PCActor pc, string name)
|
||
{
|
||
if (pc == null || pc.tfs_weapon == null) return null;
|
||
Transform first = null;
|
||
foreach (var s in pc.tfs_weapon)
|
||
{
|
||
if (s == null || !s.gameObject.activeInHierarchy) continue;
|
||
if (!string.IsNullOrEmpty(name) && s.name == name) return s;
|
||
if (first == null && s.childCount > 0) first = s;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
/// <summary>무기 끝(소켓 아래 렌더러 경계의 가장 높은 곳) — 렌더러가 없으면 소켓 자리.</summary>
|
||
static Vector3 Tip(Transform sock, WLStaffEarthFx fx)
|
||
{
|
||
if (fx != null && fx.head != 0) // #963 무기 메시 머리(끝 빛과 같은 점) — 이전 방식은 기운 지팡이에서 월드 상자 꼭대기 · 가운데라 머리에서 떨어졌다(PD 「모으는 연출이 둥근 머리에 안 모인다」)
|
||
for (int i = 0; i < sock.childCount; i++)
|
||
{
|
||
var w = sock.GetChild(i); if (!w.gameObject.activeInHierarchy) continue;
|
||
Vector3 hl; if (WLWeaponTipGlow.HeadLocal(w, sock, fx.headPull, out hl)) return w.TransformPoint(hl);
|
||
}
|
||
bool any = false; Bounds b = new Bounds(sock.position, Vector3.zero);
|
||
foreach (var r in sock.GetComponentsInChildren<Renderer>(false))
|
||
{
|
||
if (r is ParticleSystemRenderer || !r.enabled) continue;
|
||
if (!any) { b = r.bounds; any = true; } else b.Encapsulate(r.bounds);
|
||
}
|
||
return any ? new Vector3(b.center.x, b.max.y, b.center.z) : sock.position;
|
||
}
|
||
|
||
static GameObject Spawn(WLStaffEarthFx fx, Actor actor, Transform parent, Vector3 pos, float extra)
|
||
{
|
||
var fwd = actor != null ? actor.transform.forward : Vector3.forward;
|
||
var go = SpawnAt(fx, actor, pos, fwd, extra);
|
||
if (go != null && fx.follow != 0 && parent != null) go.transform.SetParent(parent, true); // 월드 크기 · 자리 유지한 채 따라감
|
||
return go;
|
||
}
|
||
|
||
internal static GameObject SpawnAt(WLStaffEarthFx fx, Actor actor, Vector3 pos, Vector3 fwd, float extra)
|
||
{
|
||
if (fx == null || string.IsNullOrEmpty(fx.prefab)) return null;
|
||
var p = Prefab(fx.prefab); if (p == null) return null;
|
||
fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward;
|
||
var basis = Quaternion.LookRotation(fwd.normalized);
|
||
var go = Object.Instantiate(p, pos + basis * V(fx.offset), basis * Quaternion.Euler(V(fx.euler)));
|
||
float s = fx.scale * (fx.bodyScale != 0 && actor != null ? WL.Combat.Impact.WLImpactFx.SizeFactor(actor) : 1f);
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
|
||
go.transform.localScale = Vector3.one * s;
|
||
float total = Mathf.Max(0f, extra) + Mathf.Max(0.1f, fx.life);
|
||
if (fx.fade > 0f) WLFxFade.Attach(go, Mathf.Max(0f, total - fx.fade), fx.fade);
|
||
else Object.Destroy(go, total);
|
||
LastLog = "생성 " + fx.prefab + " @" + pos.ToString("F1") + " ×" + s.ToString("F2");
|
||
return go;
|
||
}
|
||
|
||
// ── 주력 · 각성: 원본 Set() 훅 ─────────────────────────────────────────────────────
|
||
/// <summary>원본 Set() 훅(1줄) — 표에 있는 스킬이면 원본 진행을 건너뛰고 WL 진행을 시작한다(true). 꺼져 있거나 표에 없으면 false(원본 그대로).</summary>
|
||
public static bool TakeOver(ProjectileBase pb)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; if (c.enabled == 0 || pb == null) return false;
|
||
WLStaffEarthSkill r = null;
|
||
if (c.skills != null) foreach (var x in c.skills) if (x != null && x.skill == pb.GetType().Name) { r = x; break; }
|
||
if (r == null) return false;
|
||
var pd = pb.Get_ProjectTileData(); if (pd == null || DSUtil.CheckNull(pd.shooter)) return false;
|
||
if (s_runner == null) { var go = new GameObject("[WL935] StaffEarth"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLStaffEarthRunner>(); }
|
||
s_runner.Begin(pb, pd, r);
|
||
TakeOvers++;
|
||
return true;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
|
||
}
|
||
|
||
/// <summary>center 에서 reach(≤ radius) 안의 적(아직 안 맞은)에게 원본 투사체 피해 + 띄우기 · 기절.</summary>
|
||
internal static void HitAll(ProjectileBase pb, Actor shooter, Vector3 center, WLStaffEarthSkill r, float reach, HashSet<Actor> done)
|
||
{
|
||
var info = ActorInfo.Ins; if (info == null || pb == null || DSUtil.CheckNull(shooter)) return;
|
||
var list = info.Get_EnemyActors(shooter.IsEnemy(), center, r.radius + 3f); if (list == null) return;
|
||
try
|
||
{
|
||
for (int k = 0; k < list.Count; k++)
|
||
{
|
||
var a = list[k]; if (a == null || a.IsDead() || done.Contains(a)) continue;
|
||
var v = a.Get_position() - center; v.y = 0f;
|
||
if (v.magnitude > Mathf.Min(reach, r.radius) + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||
var col = a.GetComponent<Collider>(); if (col == null || !col.enabled) continue;
|
||
done.Add(a);
|
||
if (!pb.ProcessCollider(col, false)) continue;
|
||
Hits++;
|
||
if ((r.launchHeight > 0f || r.stunSeconds > 0f) && WLLaunch.Apply(a, r.launchHeight, r.launchSeconds, r.stunSeconds)) Launches++;
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
}
|
||
|
||
/// <summary>원본 투사체 끄기(풀 반납) — ProjectileBase.Off(bool, bool) 는 protected 라 리플렉션.</summary>
|
||
internal static void OffOriginal(ProjectileBase pb)
|
||
{
|
||
if (pb == null || !pb.gameObject.activeInHierarchy) return;
|
||
if (s_off == null) s_off = typeof(ProjectileBase).GetMethod("Off", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new[] { typeof(bool), typeof(bool) }, null);
|
||
try { if (s_off != null) s_off.Invoke(pb, new object[] { true, false }); else pb.gameObject.SetActive(false); }
|
||
catch (System.Exception ex) { Errors++; LastLog = "Off 실패 — " + ex.Message; pb.gameObject.SetActive(false); }
|
||
}
|
||
|
||
// ── 3타: 쏘는 순간 표시 → 피해 확정에 바위 솟음 · 띄우기 · 기절 ─────────────────────
|
||
/// <summary>투사체 Set 끝 훅(WLProjectileVisual.OnSet 첫 줄) — 쏘는 순간 애니메이터 상태가 popup.stateName 이면 그 투사체(인스턴스)를 표시 · 아니면 지운다(풀 재사용).</summary>
|
||
public static void OnProjectileSet(ProjectileBase pb)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; if (c.enabled == 0 || pb == null) return;
|
||
var pd = pb.Get_ProjectTileData(); var sh = pd != null ? pd.shooter as PCActor : null;
|
||
var an = sh != null ? sh.m_animation : null;
|
||
bool mark = sh != null && !pd.IsSkill() && sh.Get_ID() == c.popup.classId && an != null && an.GetCurrentAnimatorStateInfo(0).IsName(c.popup.stateName);
|
||
if (mark) { s_marked.Add(pb.GetInstanceID()); Marked++; } else s_marked.Remove(pb.GetInstanceID());
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "3타 표시 실패 — " + ex.Message; }
|
||
}
|
||
|
||
static void OnHitConfirmed(in HitConfirmedEvent e)
|
||
{
|
||
try
|
||
{
|
||
var pb = e.projectile != null ? e.projectile.m_ProjectileBase : null;
|
||
if (pb == null || DSUtil.CheckNull(e.victim) || !s_marked.Contains(pb.GetInstanceID())) return;
|
||
var p = Cfg.popup;
|
||
var fwd = !DSUtil.CheckNull(e.attacker) ? e.victim.Get_position() - e.attacker.Get_position() : e.victim.transform.forward;
|
||
SpawnAt(p.fx, e.attacker, e.victim.Get_position(), fwd, 0f);
|
||
Popups++;
|
||
if ((p.launchHeight > 0f || p.stunSeconds > 0f) && WLLaunch.Apply(e.victim, p.launchHeight, p.launchSeconds, p.stunSeconds)) Launches++;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "3타 명중 실패 — " + ex.Message; }
|
||
}
|
||
|
||
static GameObject Prefab(string path)
|
||
{
|
||
GameObject p;
|
||
if (s_prefabs.TryGetValue(path, out p) && p != null) return p;
|
||
p = Resources.Load<GameObject>(path);
|
||
if (p == null) LastLog = "이펙트 없음 " + path;
|
||
s_prefabs[path] = p;
|
||
return p;
|
||
}
|
||
|
||
static Vector3 V(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; }
|
||
}
|
||
|
||
/// <summary>#935 넘겨받은 원본 스킬 투사체 1 개의 진행(이펙트 → 판정 → 끄기). 숨김 러너 1 개가 코루틴을 돌린다(원본 투사체가 먼저 꺼져도 판정은 이어감).</summary>
|
||
public sealed class WLStaffEarthRunner : MonoBehaviour
|
||
{
|
||
public void Begin(ProjectileBase pb, ProjectileData pd, WLStaffEarthSkill r) { StartCoroutine(Run(pb, pd, r)); }
|
||
|
||
/// <summary>#963 운석 1개 — 땅에 닿는 시각(meteorImpact)에 그 자리 radius 안 적(운석마다 따로 셈)에게 원본 투사체 피해 + 기절.</summary>
|
||
IEnumerator MeteorHit(ProjectileBase pb, Actor shooter, Vector3 pt, WLStaffEarthSkill r)
|
||
{
|
||
if (r.meteorImpact > 0f) yield return new WaitForSeconds(r.meteorImpact);
|
||
if (pb == null || DSUtil.CheckNull(shooter)) yield break;
|
||
WLStaffEarth.HitAll(pb, shooter, pt, r, r.radius, new HashSet<Actor>());
|
||
}
|
||
|
||
IEnumerator Run(ProjectileBase pb, ProjectileData pd, WLStaffEarthSkill r)
|
||
{
|
||
// 원본 겉모습 · 판정 상자 · 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); }
|
||
foreach (var ps in pb.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||
bool wasEnabled = pb.enabled; pb.enabled = false;
|
||
var shooter = pd.shooter; var target = pd.target;
|
||
if (r.meteors > 0) // #963 메테오 — 운석마다 무작위 자리 · 땅에 닿는 시각에 그 자리 판정 + 기절
|
||
{
|
||
// #964 PD 「지팡이 각성 스킬이 플레이어 주위가 아니라 타겟 된 적 중심으로 3 m 범위 내 랜덤」 — 투사체 대상이 비거나 죽었으면 PC 가 겨눈 대상(Get_Target)으로 · 그것도 없으면 이전처럼 시전자 앞 3 m
|
||
WLStaffEarth.MeteorSrc = !DSUtil.CheckNull(target) && !target.IsDead() ? "투사체 대상" : "투사체 대상 없음";
|
||
if ((DSUtil.CheckNull(target) || target.IsDead()) && shooter != null) { var t2 = shooter.Get_Target(); if (!DSUtil.CheckNull(t2) && !t2.IsDead()) { target = t2; WLStaffEarth.MeteorSrc += " → PC 대상"; } else WLStaffEarth.MeteorSrc += " → 시전자 앞"; }
|
||
WLStaffEarth.MeteorPts.Clear(); WLStaffEarth.MeteorCenters.Clear();
|
||
Vector3 last = DSUtil.CheckNull(target) ? shooter.Get_position() + shooter.transform.forward * 3f : target.Get_position();
|
||
var cam = Camera.main; Vector3 cf = cam != null ? cam.transform.forward : shooter.transform.forward; cf.y = 0f; if (cf.sqrMagnitude < 1e-4f) cf = Vector3.forward;
|
||
Vector3 from = Quaternion.Euler(0f, r.meteorYaw, 0f) * cf.normalized;
|
||
for (int i = 0; i < r.meteors; i++)
|
||
{
|
||
if (r.center != "caster" && !DSUtil.CheckNull(target) && !target.IsDead()) last = target.Get_position();
|
||
Vector3 c0 = r.center == "caster" ? shooter.Get_position() : last;
|
||
Vector2 d = Random.insideUnitCircle * Mathf.Max(0f, r.meteorArea);
|
||
Vector3 pt = new Vector3(c0.x + d.x, c0.y, c0.z + d.y);
|
||
WLStaffEarth.MeteorPts.Add(pt); WLStaffEarth.MeteorCenters.Add(c0); WLStaffEarth.MeteorCenter = c0; // #964 검증용(낙하 자리 · 그때 중심)
|
||
WLStaffEarth.SpawnAt(r.fx, shooter, pt, from, 0f); WLStaffEarth.Meteors++;
|
||
StartCoroutine(MeteorHit(pb, shooter, pt, r));
|
||
if (r.meteorInterval > 0f && i < r.meteors - 1) yield return new WaitForSeconds(r.meteorInterval);
|
||
}
|
||
WLStaffEarth.LastLog = r.skill + " 메테오 " + r.meteors + "개 · 반경 " + r.meteorArea + " m";
|
||
yield return new WaitForSeconds(Mathf.Max(0f, r.meteorImpact) + Mathf.Max(0f, r.endAfter));
|
||
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;
|
||
yield break;
|
||
}
|
||
Vector3 center = r.center == "caster" || DSUtil.CheckNull(target) ? shooter.Get_position() : target.Get_position();
|
||
Vector3 fwd = center - shooter.Get_position(); if (r.center == "caster" || fwd.sqrMagnitude < 0.01f) fwd = shooter.transform.forward;
|
||
WLStaffEarth.SpawnAt(r.fx, shooter, center, fwd, 0f);
|
||
WLStaffEarth.LastLog = r.skill + " → " + r.fx.prefab + " @" + center.ToString("F1");
|
||
if (r.hitDelay > 0f) yield return new WaitForSeconds(r.hitDelay);
|
||
var done = new HashSet<Actor>();
|
||
if (r.spreadSpeed > 0f)
|
||
{
|
||
float t0 = Time.time, last = r.radius / r.spreadSpeed + 0.05f;
|
||
while (true)
|
||
{
|
||
float t = Time.time - t0;
|
||
WLStaffEarth.HitAll(pb, shooter, center, r, t * r.spreadSpeed, done);
|
||
if (t >= last) break;
|
||
yield return null;
|
||
}
|
||
}
|
||
else WLStaffEarth.HitAll(pb, shooter, center, r, r.radius, done);
|
||
if (r.endAfter > 0f) yield return new WaitForSeconds(r.endAfter);
|
||
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;
|
||
}
|
||
}
|
||
}
|