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

505 lines
33 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLRapidShot.cs — 활 주력 「속사」(Skill_RapidShot) WL 방식 (#916 · 2026-10-17)
//
// PD 원문: 「활을 쏠 때 플레이어가 뒤로 밀리지 않게」 · 「활의 주력 스킬시전할 때 플레이어의 활이 빛나며 기가 모이는 연출을 추가
// (해머 각성기 참고하되, 해머보다 임펙트가 너무 강하지는 않도록)」 · 「과녁의 크기가 몬스터의 크기를 고려해 유동적이 되도록 ·
// (이펙트가 몬스터에게 가려지지 않도록)」 · 「매우 빠르게 활쏘는 모션을 반복해 3발의 투사체를 연속으로 발사」
// ■ 원본(Skill_RapidShot.Co_Update): 과녁(Effect_BulletTarget · 대상 몸 가운데 · 크기 고정) → 1 초 → 0.25 초 간격 7 발 · 발마다 시전자 넉백
// (원본 넉백 최소 0.1 초 × 속도 4 = 한 발 약 0.4 m 뒤로 · Play 실측).
// ■ WL — 원본 Set 의 훅 1줄(Take)이 넘겨받는다:
// ① 과녁 = 원본 이펙트 그대로 · 대상 중심에서 카메라 쪽으로 몸 반지름 + markFront 앞에(몸에 가려지지 않게) · 배율 = 몹 키 × markPerHeight(markMin~markMax)
// ② 모으기 chargeSeconds — 활 당긴 자세(shotState 의 drawNormalized)로 멈추고, 활에 흰 빛(WLWeaponGlowFx · glowMax) + 활 가운데 모으기 이펙트(chargeFx)
// ③ shots 발 — 발마다 shotState 를 (첫 발 drawNormalized · 다음 fromNormalized) 부터 shotSpeed 배로 재생 → 놓는 순간(클립 Projectile 이벤트)에
// 원본 투사체 1 발(관통 · 원본과 같은 한 발 피해) · 대상을 본다 · 시전자 넉백 없음
// ④ 끝 — endNormalized 까지 재생 → 애니메이터 되돌림 · 대기 · 빛 glowFadeSeconds 동안 꺼짐 · 투사체 Off. 이동 · 구르기로 스킬 상태가 끊기면 즉시 끝(스킵).
// ■ noRecoil = 쏠 때 시전자를 뒤로 미는 다른 활 스킬(원본 Set 훅 1줄 · 속박의 화살 Skill_BindingArrow 0.05 초 × 속도 10)
// ■ #975 multi = 활 기본 스킬(원본 Skill_MultiShot 703404 = 일반 공격 화살 추가 버프) — PD 「주력 스킬과 동일하게 모으기 이펙트 시전 후 전방으로 부채꼴 모양으로
// 퍼지는 화살 10발을 3회 연속으로 발사(멀티샷 형태 · 0.5초 간격)」 — 원본 Set 훅 1줄(TakeMulti)이 넘겨받아 ②모으기는 주력과 같고 ③에서 volleys 번 ·
// interval 초마다(놓는 순간 기준) 대상 쪽(없으면 정면) 부채꼴 fanDegrees 에 arrows 발(화살 = multi.projectile · 겉모습 WLProjectileVisual · 사거리 = 흰 원 × rangeMul)
// ■ 값 = Resources/WL/WLRapidShot.json (C45) · 끄기 = enabled 0 → 원본 동작
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLRapidShotJson
{
public int enabled = 0;
public int[] classes = new int[0]; // 비우면 모든 직업
public int shots = 3;
public string shotState = "attack";
public string shotClip = "FR_Bow1_10402"; // 놓는 순간 = 이 클립의 Projectile 이벤트
public float drawNormalized = 0.8f; // 모으기 동안 멈출 자세(활 당김) · 첫 발 시작
// #967 PD 「주력 스킬 모으기 시 활을 들지 않는 손이 멈춰 있는데 — 활을 쏠 때와 같이 자연스럽게 활시위를 당기는 포즈로 모으도록」 — chargeFromNormalized ≥ 0 이면 모으기 첫 chargeDrawSeconds 동안
// shotState 를 그 자리(활 든 채 시위에 손)부터 drawNormalized(끝까지 당김)까지 재생한 뒤 유지 · < 0 = 이전(처음부터 drawNormalized 정지)
public float chargeFromNormalized = -1f;
public float chargeDrawSeconds = 0.35f;
public float releaseNormalized; // #967 > 0 = 놓는 순간(발사)을 이 정규화 시각으로 · 0 = shotClip 의 Projectile 이벤트
public float fromNormalized = 0.55f; // 둘째 발부터 여기서 재생
public float shotSpeed = 2.5f; // 발 재생 배속
public float endNormalized = 0.97f; // 마지막 발 뒤 여기까지 재생하고 대기
public float chargeSeconds = 0.6f;
public string socket = "w_archer_l"; // 활 소켓
public string chargeFx = "";
public float chargeFxScale = 0.06f;
public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow";
public float[] glowColor = { 1.2f, 1.3f, 1.5f };
public float glowMax = 0.6f, glowFill = 0.3f, glowRimPower = 2.5f, glowFadeSeconds = 0.2f;
public float markFront = 0.15f; // 과녁을 몸 반지름 + 이 거리(m)만큼 카메라 쪽 앞에
public float markPerHeight = 0.35f; // 과녁 배율 = 몹 키(m) × 이 값
public float markMin = 0.3f, markMax = 2f;
public float markExtraLife = 0.3f; // 마지막 발 뒤 과녁 유지(초)
public string[] noRecoil = new string[0]; // 쏠 때 시전자 넉백을 하지 않을 다른 스킬(eEffect 이름)
// #921 PD 「이펙트 궤적이 타겟 방향으로만 집중해서 날아가도록 · 그 방향으로 화살 기본 공격의 사정거리만큼 직선상 경로상 모든 적에게 피해(레이저 판정)」
public int laser = 0; // 1 = 발마다 PC → 대상 방향 직선(길이 = 기본 공격 사정거리 · 흰 원) 위 모든 적에게 그 화살의 한 발 피해를 즉시 · 화살은 판정 없이 사정거리까지만 곧게 날아간다
public float laserWidth = 0.6f; // 직선 판정 폭(m) — 적 몸 반지름은 따로 더한다
public int aimBody = 1; // 1 = 화살이 대상 몸 중심(공중 몹은 그림 가운데) 쪽으로 곧게
public int chargeBlocksMove = 0; // #922 PD 「주력 스킬 모으기 이펙트 재생중에는 이동하지 못하게(구르기로 취소는 가능)」 — 1 = 모으기 동안 이동 입력 무시(WLSkillCancel.HoldMove · 구르기 = 스킵)
public int holdThroughShots = 0; // #922 1 = 막기를 마지막 발까지(모으기만 막으면 이동 키를 누른 채일 때 모으기 직후 이동이 들어와 한 발도 안 쏘고 끊겼다 · Play 실측)
public float holdMaxSeconds = 3f; // 막기 상한(초) — 끝나면 어떻게든 푼다
public WLRapidShotMulti multi = new WLRapidShotMulti(); // #975 활 기본 스킬(멀티샷)
public string note = "";
}
/// <summary>#975 활 기본 스킬 — 주력과 같은 모으기 뒤 volleys 번 · interval 초 간격(놓는 순간)으로 앞쪽 부채꼴 fanDegrees 에 화살 arrows 발.</summary>
[System.Serializable]
public class WLRapidShotMulti
{
public int enabled = 0;
public string projectile = "Projectile_ArcherSkill2Arrow"; // 화살(주력 속사와 같은 화살 · 겉모습 = WLProjectileVisual 표)
public int volleys = 3;
public float interval = 0.5f;
public int arrows = 10;
public float fanDegrees = 60f; // 부채꼴 전체 각(°) — 화살 사이 = fanDegrees / (arrows − 1)
public float rangeMul = 1f; // 나는 거리 = 기본 공격 사정거리(흰 원) × 이 값 · 0 = 투사체 원래 수명
public int pierce = 0; // 1 = 관통
public int mark = 0; // 1 = 과녁 표시(주력처럼)
public float drawFraction = 0.8f; // #976 다시 당기는 시간 상한 = interval × 이 값(간격이 당김 동작 0.23 s 보다 짧으면 그만큼 빨리 당긴다)
public int noExtra = 1; // #977 1 = 화살에 원본 스킬 부가 효과(703404 방어력 감소 디버프) 없음
public string note = "";
}
public static class WLRapidShot
{
public const string ResourcesPath = "WL/WLRapidShot";
public static int Taken, Shots, Aborted, RecoilSkipped, Errors, Lasers, LaserHits, TakenMulti, MultiArrows; public static string LastLog = "";
static WLRapidShotJson s_cfg;
internal static WLRapidShotJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLRapidShotJson();
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.SubsystemRegistration)] static void Reset() { s_cfg = null; }
static int ClassOf(PCActor pc) { return pc != null ? pc.Get_ID() : 0; }
/// <summary>원본 Skill_RapidShot.Set 훅 — true 면 WL 이 이 투사체를 맡는다(원본 Co_Update 건너뜀).</summary>
public static bool Take(ProjectileBase pb, List<ProjectileData> shots, string projectile)
{
try
{
var c = Cfg;
if (c.enabled == 0 || pb == null || shots == null || shots.Count == 0) return false;
var data = pb.Get_ProjectTileData(); var pc = data != null ? data.shooter as PCActor : null;
if (pc == null || pc.m_animation == null) return false;
if (c.classes != null && c.classes.Length > 0 && System.Array.IndexOf(c.classes, ClassOf(pc)) < 0) return false;
var run = pb.GetComponent<WLRapidShotRunner>();
if (run == null) run = pb.gameObject.AddComponent<WLRapidShotRunner>();
run.Begin(pb, pc, data.target, shots, projectile, c);
Taken++;
return true;
}
catch (System.Exception ex) { Errors++; LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
}
/// <summary>#975 원본 Skill_MultiShot.Set 훅 — true 면 WL 이 맡는다(원본 버프 건너뜀). 화살마다 스킬 투사체 데이터 복사본(시작 = 시전자 가운데).</summary>
public static bool TakeMulti(ProjectileBase pb, ProjectileData data)
{
try
{
var c = Cfg; var m = c.multi;
if (c.enabled == 0 || m == null || m.enabled == 0 || pb == null || data == null) return false;
var pc = data.shooter as PCActor;
if (pc == null || pc.m_animation == null) return false;
if (c.classes != null && c.classes.Length > 0 && System.Array.IndexOf(c.classes, ClassOf(pc)) < 0) return false;
int n = Mathf.Max(1, m.volleys) * Mathf.Max(1, m.arrows);
var list = new List<ProjectileData>(n);
for (int i = 0; i < n; i++)
{
var d = new ProjectileData(); data.Copy(d); d.eStartPos = eProjectileStartPos.ShooterCenter;
if (m.noExtra != 0) { d.e_SkillExtraType = eSkillExtraType.None; d.m_skillTypeConfigTable = null; d.SetSkillExtraType = false; } // #977 PD 「활 기본 스킬에 명중 된 적에게 발생하는 디버프를 제거」 — Actor.Check_Extra 가 None 이면 바로 돌아간다 · SetSkillExtraType 를 꺼야 ProjectileBase.Set 이 스킬 표 값으로 다시 넣지 않는다(1차 Play 실측 방어 감소 남음)
list.Add(d);
}
var target = data.target;
if (DSUtil.CheckNull(target) || target.IsDead()) target = pc.Get_Target();
var run = pb.GetComponent<WLRapidShotRunner>();
if (run == null) run = pb.gameObject.AddComponent<WLRapidShotRunner>();
run.BeginMulti(pb, pc, target, list, c);
TakenMulti++;
return true;
}
catch (System.Exception ex) { Errors++; LastLog = "멀티샷 넘겨받기 실패 — " + ex.Message; return false; }
}
/// <summary>원본 활 스킬 Set 훅 — true 면 시전자 넉백(쏠 때 뒤로 밀림)을 하지 않는다.</summary>
public static bool NoRecoil(Actor shooter, string skill)
{
var c = Cfg;
if (c.enabled == 0 || c.noRecoil == null || System.Array.IndexOf(c.noRecoil, skill) < 0 || !(shooter is PCActor)) return false;
RecoilSkipped++;
return true;
}
}
/// <summary>넘겨받은 속사 투사체 1개의 진행 — 과녁 · 모으기 · 빠른 연사 · 끝.</summary>
internal sealed class WLRapidShotRunner : MonoBehaviour
{
ProjectileBase _pb; PCActor _pc; Actor _target; List<ProjectileData> _shots; string _proj; WLRapidShotJson _c;
Animator _an; bool _animOwned; Coroutine _co;
readonly WLWeaponGlowFx _glow = new WLWeaponGlowFx();
Transform _socket; Vector3 _centerLocal; GameObject _charge;
static readonly Dictionary<string, GameObject> s_fx = new Dictionary<string, GameObject>();
bool _multi; // #975 활 기본 스킬(부채꼴 다발)
public void Begin(ProjectileBase pb, PCActor pc, Actor target, List<ProjectileData> shots, string proj, WLRapidShotJson c)
{
_pb = pb; _pc = pc; _target = target; _shots = shots; _proj = proj; _c = c; _an = pc.m_animation; _multi = false;
if (_co != null) StopCoroutine(_co);
_co = StartCoroutine(Run());
}
/// <summary>#975 활 기본 스킬 — shots = 화살마다 데이터(volleys × arrows).</summary>
public void BeginMulti(ProjectileBase pb, PCActor pc, Actor target, List<ProjectileData> shots, WLRapidShotJson c)
{
_pb = pb; _pc = pc; _target = target; _shots = shots; _proj = c.multi.projectile; _c = c; _an = pc.m_animation; _multi = true;
if (_co != null) StopCoroutine(_co);
_co = StartCoroutine(Run());
}
IEnumerator Run()
{
var c = _c; var an = _an; var mc = c.multi;
float releaseN = c.releaseNormalized > 0f ? c.releaseNormalized : ReleaseNormalized(an, c.shotClip);
if (releaseN <= 0f) { WLRapidShot.LastLog = "놓는 이벤트 없음 " + c.shotClip; releaseN = 0.92f; }
int n = _multi ? Mathf.Max(1, mc.volleys) : Mathf.Clamp(c.shots, 1, _shots.Count);
// ① 과녁 — 몹 크기에 맞춘 배율 · 카메라 쪽 앞(몸에 가려지지 않게) · #975 기본 스킬(부채꼴)은 mark 1 일 때만
if (!_multi || mc.mark != 0) ShowMark(c.chargeSeconds + n * (_multi ? mc.interval : 0.2f) + c.markExtraLife);
// ② 모으기 — 활 당긴 자세 · 활 흰 빛 · 모으기 이펙트
an.fireEvents = false; _animOwned = true;
bool drawing = c.chargeFromNormalized >= 0f && c.chargeFromNormalized < c.drawNormalized; // #967 시위를 당기며 모은다
float poseN = drawing ? c.chargeFromNormalized : c.drawNormalized;
an.Play(c.shotState, 0, poseN); an.speed = 0f; an.Update(0f);
Face();
_socket = FindSocket(_pc, c.socket);
if (_socket != null) { _glow.Begin(_socket, c.glowMaterial); _centerLocal = WLWeaponGlowFx.CenterLocal(_socket); }
_charge = Spawn(c.chargeFx, BowCenter(), c.chargeFxScale, c.chargeSeconds + 0.5f);
if (c.chargeBlocksMove != 0) { WLSkillCancel.HoldMove(_pc, c.holdThroughShots != 0 ? Mathf.Max(c.chargeSeconds, c.holdMaxSeconds) : c.chargeSeconds); _holding = true; } // #922 모으기(· 마지막 발까지) 이동 무시(구르기 = 스킵)
var col = Rgb(c.glowColor, 1f);
float t = 0f;
while (t < c.chargeSeconds)
{
if (Broken()) { End(false); yield break; }
if (an.speed != 0f) an.speed = 0f;
if (drawing) // #967 시위 당기기 — chargeDrawSeconds 동안 부드럽게 끝까지 → 유지
{
float dk = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(t / Mathf.Max(0.01f, c.chargeDrawSeconds)));
float want = Mathf.Lerp(c.chargeFromNormalized, c.drawNormalized, dk);
if (Mathf.Abs(want - poseN) > 1e-4f || !an.GetCurrentAnimatorStateInfo(0).IsName(c.shotState)) { poseN = want; an.Play(c.shotState, 0, poseN); }
}
else if (!an.GetCurrentAnimatorStateInfo(0).IsName(c.shotState)) an.Play(c.shotState, 0, c.drawNormalized); // 다른 연출이 상태를 바꾸면 되돌림
float k = Mathf.Clamp01(t / Mathf.Max(0.01f, c.chargeSeconds));
_glow.Set(c.glowMax * k, col, c.glowFill, c.glowRimPower);
if (_charge != null) _charge.transform.position = BowCenter();
t += Time.deltaTime; yield return null;
}
_glow.Set(c.glowMax, col, c.glowFill, c.glowRimPower);
if (_charge != null) { Destroy(_charge); _charge = null; }
if (_holding && c.holdThroughShots == 0) { _holding = false; WLSkillCancel.EndHoldMove(_pc, true); } // #922 모으기 끝 — 그동안 미룬 이동 입력 적용(마지막 발까지 막을 때는 연사 뒤에)
// ③ 빠른 연사 n 발 · #975 기본 스킬 = n 번 부채꼴 다발(놓는 순간 사이 mc.interval 초)
float lastFire = -99f;
float shotSp = c.shotSpeed; // 발 재생 속도(애니메이터 속도를 직접 씀 = 기본 공격 공속 배율과 무관 · #975 실측 활 컨트롤러 재생 배수 파라미터 없음)
float drawDur = Mathf.Max(0f, (releaseN - c.fromNormalized) * ClipLen(an, c.shotClip) / Mathf.Max(0.1f, c.shotSpeed)); // 다시 당겨 놓기까지(초 · 어림 — 실제 놓는 순간은 아래에서 interval 에 맞춤)
// #976 간격(0.2 s)이 당김 동작보다 짧으면 다시 당기기를 interval × drawFraction 안에 끝나게 빨리 재생
float mDraw = _multi && mc.drawFraction > 0f ? Mathf.Min(drawDur, mc.interval * mc.drawFraction) : drawDur;
float mSp = mDraw > 1e-3f && drawDur > 1e-3f ? c.shotSpeed * drawDur / mDraw : c.shotSpeed;
for (int i = 0; i < n; i++)
{
if (_multi && i > 0) // #975 앞 다발을 놓은 뒤 자세를 이어 재생하다가 interval − 당기는 시간(− 몇 프레임 여유 · 첫 Play 실측 0.54 s = 2 프레임 늦음)에 다시 당긴다
while (Time.time < lastFire + mc.interval - mDraw - 3f * Time.smoothDeltaTime)
{
if (Broken()) { End(false); yield break; }
an.speed = an.GetCurrentAnimatorStateInfo(0).normalizedTime < c.endNormalized ? shotSp : 0f; // 다음 상태로 넘어가지 않게(공격 2타 전이 0.98)
yield return null;
}
Face();
float startN = i == 0 ? c.drawNormalized : c.fromNormalized, s0 = Time.time;
float spNow = _multi && i > 0 ? mSp : shotSp; // #976 멀티샷 다시 당기기 = mSp
an.Play(c.shotState, 0, startN); an.speed = Mathf.Max(0.1f, spNow);
yield return null;
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < releaseN && Time.time - s0 < 1f)
{
if (Broken()) { End(false); yield break; }
if (an.speed != spNow) an.speed = spNow;
if (!an.GetCurrentAnimatorStateInfo(0).IsName(c.shotState)) an.Play(c.shotState, 0, startN);
yield return null;
}
if (_multi)
{
while (i > 0 && Time.time < lastFire + mc.interval) { if (Broken()) { End(false); yield break; } an.speed = 0f; yield return null; } // 놓는 순간을 interval 에 맞춤
an.speed = shotSp;
Fan(i, Mathf.Max(1, mc.arrows));
}
else
{
var data = _shots[i];
ProjectileInfo.Ins.Shoot_Projectile(_proj, data, p => { p.m_Pierce = true; WLProjectileVisual.ApplyTo(p, _proj); if (_c.laser != 0) Laser(p); }); // 원본과 같은 한 발 · 시전자 넉백 없음 · #917 겉모습 · 명중 이펙트(WLProjectileVisual 표) · #921 레이저 판정
}
lastFire = Time.time;
WLRapidShot.Shots++;
}
if (_holding) { _holding = false; WLSkillCancel.EndHoldMove(_pc, true); } // #922 마지막 발 — 미룬 이동 입력 적용
// ④ 마무리 — 빛 꺼짐 · 끝 자세
float f0 = Time.time;
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < c.endNormalized && Time.time - f0 < 0.5f)
{
if (Broken()) { End(false); yield break; }
float k = 1f - Mathf.Clamp01((Time.time - f0) / Mathf.Max(0.01f, c.glowFadeSeconds));
_glow.Set(c.glowMax * k, col, c.glowFill, c.glowRimPower);
yield return null;
}
End(true);
}
/// <summary>#921 레이저 판정 — PC 발에서 대상 쪽 수평 방향으로 기본 공격 사정거리(흰 원 WLRangeRing.AttackReach)만큼의 직선 위(폭 laserWidth + 몸 반지름) 모든 적에게
/// 이 화살의 원본 피해 경로(ProcessCollider · 끄지 않음)를 즉시 · 화살은 대상 몸 중심 쪽으로 곧게 사정거리까지만 날고 자기 판정은 끈다(이중 피해 방지).</summary>
void Laser(ProjectileBase p)
{
var c = _c; if (p == null || _pc == null) return;
float range = WL.Combat.RangeRing.WLRangeRing.AttackReach(_pc);
if (range <= 0f) return;
Vector3 origin = _pc.Get_position();
Vector3 dir = _pc.transform.forward; dir.y = 0f;
bool hasTarget = !DSUtil.CheckNull(_target) && !_target.IsDead();
if (hasTarget) { var to = _target.Get_position() - origin; to.y = 0f; if (to.sqrMagnitude > 1e-4f) dir = to; }
if (dir.sqrMagnitude < 1e-6f) return;
dir.Normalize();
// ① 겉모습 — 대상 몸 중심 쪽으로 곧게 · 사정거리 끝에서 사라짐
Vector3 look = dir;
if (c.aimBody != 0 && hasTarget)
{
var v = WL.Combat.Impact.WLImpactFx.BodyCenter(_target) - p.transform.position; var vf = v; vf.y = 0f;
if (vf.sqrMagnitude > 0.01f) look = v;
}
p.transform.rotation = Quaternion.LookRotation(look);
var pd = p.Get_ProjectTileData();
var start = p.transform.position - origin; start.y = 0f;
if (pd != null && pd.Speed > 0f) pd.LifeTime = Mathf.Max(0.05f, (range - Vector3.Dot(start, dir)) / pd.Speed);
// ② 판정 — 직선 위 모든 적
int hits = 0; var info = ActorInfo.Ins;
var list = info != null ? info.Get_EnemyActors(_pc.IsEnemy(), origin, range + 2f) : null;
if (list != null)
{
for (int k = 0; k < list.Count; k++)
{
var a = list[k];
if (a == null || a.IsDead()) continue;
var v = a.Get_position() - origin; v.y = 0f;
float body = WL.Combat.WLCombatMotionSettings.BodyRadius(a), t = Vector3.Dot(v, dir);
if (t < -body || t > range + body) continue;
if ((v - dir * t).magnitude > c.laserWidth * 0.5f + body) continue;
var col = a.GetComponent<Collider>();
if (col == null || !col.enabled) continue;
if (p.ProcessCollider(col, false)) hits++;
}
info.Release_Actors(list);
}
WLLaserMute.Mute(p);
WLRapidShot.LaserHits += hits; WLRapidShot.Lasers++;
WLRapidShot.LastLog = "레이저 " + range.ToString("F2") + " m · 맞음 " + hits;
}
/// <summary>#975 부채꼴 다발 1번 — 대상 쪽(없으면 정면) 수평 방향을 가운데로 fanDegrees 에 count 발을 고르게 · 화살은 그 방향으로 곧게 · 사거리 = 흰 원 × rangeMul.</summary>
void Fan(int volley, int count)
{
var mc = _c.multi; var pc = _pc; if (pc == null || ProjectileInfo.Ins == null) return;
Vector3 dir = pc.transform.forward; dir.y = 0f;
if (!DSUtil.CheckNull(_target) && !_target.IsDead()) { var to = _target.Get_position() - pc.Get_position(); to.y = 0f; if (to.sqrMagnitude > 1e-4f) dir = to; }
if (dir.sqrMagnitude < 1e-6f) dir = Vector3.forward;
dir.Normalize();
float range = mc.rangeMul > 0f ? WL.Combat.RangeRing.WLRangeRing.AttackReach(pc) * mc.rangeMul : 0f;
float step = count > 1 ? mc.fanDegrees / (count - 1) : 0f, a0 = count > 1 ? -mc.fanDegrees * 0.5f : 0f;
for (int k = 0; k < count; k++)
{
int idx = volley * count + k; if (idx >= _shots.Count) break;
var look = Quaternion.AngleAxis(a0 + step * k, Vector3.up) * dir;
ProjectileInfo.Ins.Shoot_Projectile(_proj, _shots[idx], p =>
{
p.m_Pierce = mc.pierce != 0; WLProjectileVisual.ApplyTo(p, _proj);
p.transform.rotation = Quaternion.LookRotation(look);
var pd = p.Get_ProjectTileData();
if (range > 0f && pd != null && pd.Speed > 0f) pd.LifeTime = Mathf.Max(0.05f, range / pd.Speed);
});
WLRapidShot.MultiArrows++;
}
WLRapidShot.LastLog = "멀티샷 " + (volley + 1) + "번째 · " + count + "발 · 부채 " + mc.fanDegrees.ToString("0.#") + "° · 사거리 " + range.ToString("F1") + " m";
}
static float ClipLen(Animator an, string clipName)
{
if (an != null && an.runtimeAnimatorController != null)
foreach (var clip in an.runtimeAnimatorController.animationClips) if (clip != null && clip.name == clipName && clip.length > 0f) return clip.length;
return 1f;
}
bool Broken() { return _pc == null || _pc.IsDead() || _pc.Get_CurAnim() != eAnim.Skill; }
bool _holding;
void End(bool toIdle)
{
if (_holding) { _holding = false; WLSkillCancel.EndHoldMove(_pc, toIdle); }
if (!toIdle) { WLRapidShot.Aborted++; WLRapidShot.LastLog = "끊김(이동 · 구르기)"; }
if (_animOwned && _an != null) { _an.speed = 1f; _an.fireEvents = true; }
_animOwned = false; _co = null;
if (toIdle && _pc != null && !_pc.IsDead() && _pc.Get_CurAnim() == eAnim.Skill) _pc.Play_Idle();
_glow.End();
if (_charge != null) { Destroy(_charge); _charge = null; }
WLSlashFlurry.Off(_pb);
}
void OnDisable()
{
if (_holding) { _holding = false; WLSkillCancel.EndHoldMove(_pc, false); }
if (_co != null) { StopCoroutine(_co); _co = null; }
if (_animOwned && _an != null) { _an.speed = 1f; _an.fireEvents = true; _animOwned = false; }
_glow.End();
if (_charge != null) { Destroy(_charge); _charge = null; }
}
void Face()
{
if (_pc == null || DSUtil.CheckNull(_target) || _target.IsDead()) return;
var to = _target.Get_position() - _pc.Get_position(); to.y = 0f;
if (to.sqrMagnitude > 1e-4f) _pc.transform.rotation = Quaternion.LookRotation(to);
}
Vector3 BowCenter()
{
if (_socket != null) return _socket.TransformPoint(_centerLocal);
return _pc != null ? _pc.Get_Center_position() : transform.position;
}
void ShowMark(float life)
{
if (DSUtil.CheckNull(_target) || InGameInfo.Ins == null) return;
var c = _c; var target = _target;
float h = Mathf.Max(0.1f, target.Get_Top_postion().y - target.Get_position().y);
float scale = Mathf.Clamp(h * c.markPerHeight, c.markMin, c.markMax);
InGameInfo.Ins.Show_Effect("Effect_BulletTarget", target, eEffectLocation.Mid, life, off =>
{
if (off == null) return;
var fit = off.GetComponent<WLMarkFit>(); if (fit == null) fit = off.gameObject.AddComponent<WLMarkFit>();
fit.Begin(target, c.markFront, scale, life);
});
}
static Transform FindSocket(PCActor pc, string name)
{
if (pc == null || pc.tfs_weapon == null) return null;
foreach (var s in pc.tfs_weapon) if (s != null && s.name == name && s.gameObject.activeInHierarchy) return s;
return null;
}
static float ReleaseNormalized(Animator an, string clipName)
{
if (an == null || an.runtimeAnimatorController == null) return -1f;
foreach (var clip in an.runtimeAnimatorController.animationClips)
{
if (clip == null || clip.name != clipName || clip.length <= 0f) continue;
foreach (var ev in clip.events) if (ev.functionName == "Projectile") return ev.time / clip.length;
}
return -1f;
}
static GameObject Spawn(string path, Vector3 pos, float scale, float life)
{
if (string.IsNullOrEmpty(path)) return null;
GameObject prefab;
if (!s_fx.TryGetValue(path, out prefab)) { prefab = Resources.Load<GameObject>(path); s_fx[path] = prefab; }
if (prefab == null) { WLRapidShot.LastLog = "이펙트 없음 " + path; return null; }
var go = Instantiate(prefab, pos, prefab.transform.rotation);
go.transform.localScale = prefab.transform.localScale * Mathf.Max(0.01f, scale);
Destroy(go, Mathf.Max(0.1f, life));
return go;
}
static Color Rgb(float[] a, float d) { return new Color(a != null && a.Length > 0 ? a[0] : d, a != null && a.Length > 1 ? a[1] : d, a != null && a.Length > 2 ? a[2] : d, 1f); }
}
/// <summary>#916 과녁 맞춤 — 원본 TurnOff_GO 가 Update 에서 대상 가운데로 옮긴 뒤(LateUpdate) 카메라 쪽 앞으로 당기고 몹 크기 배율을 준다. 기간이 지나면 원래 배율.</summary>
public sealed class WLMarkFit : MonoBehaviour
{
Actor _target; float _front, _scale = 1f, _until; bool _on; Vector3 _scale0 = Vector3.one; bool _hasScale0;
public void Begin(Actor target, float front, float scale, float life)
{
if (!_hasScale0) { _scale0 = transform.localScale; _hasScale0 = true; }
_target = target; _front = front; _scale = scale; _until = Time.time + life; _on = true;
foreach (var ps in GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
Apply();
}
void LateUpdate() { if (_on) Apply(); }
void Apply()
{
if (Time.time > _until || DSUtil.CheckNull(_target) || _target.IsDead()) { Stop(); return; }
var mid = WL.Combat.Impact.WLImpactFx.BodyCenter(_target); // #921 PD 「몸 정중앙 · 공중 몬스터라도」 — 원본 Center_position(등장 때 잰 고정 높이) 대신 지금 몸 중심(떠 있는 몹 = 지금 그림 가운데)
var cam = Camera.main;
var toCam = cam != null ? (cam.transform.position - mid).normalized : Vector3.up;
transform.position = mid + toCam * (WL.Combat.WLCombatMotionSettings.BodyRadius(_target) + _front);
transform.localScale = _scale0 * _scale;
}
void Stop() { _on = false; if (_hasScale0) transform.localScale = _scale0; }
void OnDisable() { Stop(); }
}
/// <summary>#921 레이저로 피해를 준 속사 화살 — 날아가는 동안 자기 판정을 끈다(이중 피해 방지) · 풀로 돌아갈 때(꺼질 때) 되살린다.</summary>
public sealed class WLLaserMute : MonoBehaviour
{
Collider[] _cols;
public static void Mute(ProjectileBase p)
{
if (p == null) return;
var m = p.GetComponent<WLLaserMute>(); if (m == null) m = p.gameObject.AddComponent<WLLaserMute>();
if (m._cols == null) m._cols = p.GetComponentsInChildren<Collider>(true);
for (int i = 0; i < m._cols.Length; i++) if (m._cols[i] != null) m._cols[i].enabled = false;
m.enabled = true;
}
void OnDisable()
{
if (_cols != null) for (int i = 0; i < _cols.Length; i++) if (_cols[i] != null) _cols[i].enabled = true;
enabled = false;
}
}
}