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

505 lines
33 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}