451 lines
30 KiB
C#
451 lines
30 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLHammerQuake.cs — 해머 각성 「대지 분쇄」 (#911 · #912 · 2026-10-17) — 파츠 스킬 행동 hammer_quake
|
||
//
|
||
// PD 원문: 「첫 타 내려치기 모션 직전(망치를 든 상태)를 0.5초 재생하고, 이때 망치에 이펙트 재생(Master Stylized Projectile 에셋의 모으기를 망치에 ·
|
||
// 망치 크기에 맞게) · 이후 내려치기 모션을 빠르게 3회 반복하며, 전방으로 Stylized Earth Skills 에셋의 충격파를 내려친 방향의 전방 방사형
|
||
// 3갈래로 3번 연속 내뻗으며 공격하고, 피격 된 적을 공중에 타격받을 때마다 2미터씩 띄우고 적들에게 2초의 스턴 ·
|
||
// 망치로 내려친 지면은 FX_EarthSkill_Popup_05 에서 바위 형태만 숨긴 상태로 망치가 내려친 위치에」 → 「내려치기 횟수를 3회가 아니라 1회로」
|
||
// #912 PD: 「모으기 이펙트가 재생될 때 망치가 점차 하얗게 빛날 수 있게」 · 「모으기 이펙트가 2D 같은 느낌 · 좀 더 빛나는 느낌이 나게」 ·
|
||
// 「망치를 내려치기 직전까지 망치를 1.5배까지 크기를 키우고」 · 「망치 내려친 후 바로 움직이지 않고 임펙트 있게 내려친 상태로 캐릭터가 잠시 멈추게(0.5초 내외)」
|
||
// #913 PD: 「모으기 공격의 이펙트 위치가 망치 머리에」 · 「빛 광원이 주변을 비추지 않고 망치 색상만 빛나게(완전 하얗게 빛나지 않고 망치의 외형이 살짝은 보일 수 있도록)」 ·
|
||
// 「망치 각성 스킬을 내려칠 때도 광원이 비추면 안돼」 → 모으기 이펙트 · 내려친 지점 = 망치 머리 중심(HeadCenter) · 점광원 lightIntensity 0(만들지 않음) ·
|
||
// 흰 빛 몸통 덮기 glowFill · 테두리 glowRimPower 를 JSON 으로
|
||
// #914 PD: 「원형 모이는 이펙트 위치를 망치 기준 조금 더 아래로(원형 이펙트가 망치 머리에 거의 가려지도록)」 · 「내려친 위치의 이펙트 크기를 조금 더 키워주고
|
||
// 망치 주변 반경에 있는 적들도 광역 피해」 · 「해머를 내려친 위치를 기준으로 원형 3m 범위 내 적에게도 동일한 피해」 → chargeFxDrop · groundFxScale ·
|
||
// impactRadius 3 m(내려친 순간 · 충격파와 같은 피해 + 띄움 · 기절 · 같은 내려치기에서 적 1 회)
|
||
// ■ 흐름 — 원본 Play_Skill(1타 상태)로 스킬 상태(자동 공격 · 이동 끼어들기 없음 · 대상 보기) → 애니메이터 이벤트를 끄고(1타 판정 · 이펙트가 같이 나가지 않게)
|
||
// ① holdNormalized 자세로 멈춰 chargeSeconds 동안 — 망치(쥔 손 = 소켓 원점 기준)가 1 → growScale 배로 커지고 · 흰 빛(WL/WeaponGlow 덧그림 · HDR → 블룸)이
|
||
// 0 → glowMax · 망치 끝에 모으기 이펙트 + 점광원(lightIntensity · 주변을 비춘다)
|
||
// ② 내려치기를 slams 번: holdNormalized → 타격 시점(클립 Projectile 이벤트)에서 지면 이펙트 + 3 갈래 충격파(prongAngles) → slamEndNormalized 까지 · 배속 slamSpeed.
|
||
// 마지막 한 번은 내려친 자세(impactHoldNormalized · 망치가 땅에 닿은 프레임)에서 impactHoldSeconds 멈춤 — 그동안 이동 입력 무시(구르기 = 스킵 · WLSkillCancel.HoldMove) ·
|
||
// 흰 빛 · 점광원은 내려친 뒤 glowFadeSeconds 동안 꺼진다
|
||
// ③ 끝 = 이벤트 · 속도 되돌리고 대기 · 망치는 shrinkSeconds 동안 원래 크기.
|
||
// ■ 충격파 판정 = 갈래마다 앞으로 prongTravelSeconds 동안 prongLength 까지 나아가는 앞줄 · 폭 prongWidth(+ 몸 반지름) 안의 적 · 한 번 내려칠 때 적 1 회 ·
|
||
// 피해 = 레벨표 dmg_pct · 맞을 때마다 launchHeight m 띄우고 내려온 뒤 stunSeconds 기절(WLLaunch.Apply · 보스 제외).
|
||
// ■ 이동 · 구르기로 스킬 상태가 끊기면(WLSkillCancel 기본 = 스킵) 남은 내려치기 · 이펙트 · 판정을 바로 멈춘다.
|
||
// ■ 값 = Resources/WL/WLHammerQuake.json (C45) · 쿨타임 · 피해 = WLPartSkillLevelsWL.csv 900201 · 정의 = WLPartSkillDefs.csv hammer_quake
|
||
// 🔴 Debug.LogError 금지(원본 팝업).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace WL.PartSkill
|
||
{
|
||
[System.Serializable]
|
||
public class WLHammerQuakeJson
|
||
{
|
||
public int enabled = 1;
|
||
public string slamState = "attack";
|
||
public string slamClip = "Attack1_S_10501";
|
||
public float holdNormalized = 0.27f;
|
||
public float chargeSeconds = 0.5f;
|
||
public int slams = 3;
|
||
public float slamSpeed = 1.6f;
|
||
public float slamEndNormalized = 0.55f;
|
||
public string chargeFx = "WL/Fx/WL_Fx_HammerCharge";
|
||
public float chargeFxScale = 0.3f;
|
||
public string groundFx = "WL/Fx/WL_Fx_QuakeGround";
|
||
public float groundFxScale = 0.35f;
|
||
public float groundFxLife = 2.5f;
|
||
public string pathFx = "WL/Fx/WL_Fx_QuakePath";
|
||
public float pathFxNativeLength = 8f;
|
||
public float pathFxLife = 2.5f;
|
||
public float[] prongAngles = { -30f, 0f, 30f };
|
||
public float prongLength = 4.5f;
|
||
public float prongWidth = 1.0f;
|
||
public float prongTravelSeconds = 0.4f;
|
||
public float strikeForward = 0.85f;
|
||
public float launchHeight = 2f;
|
||
public float launchSeconds = 0.7f;
|
||
public float stunSeconds = 2f;
|
||
// #912 망치 연출 · 내려친 자세 멈춤
|
||
public float growScale = 1.5f; // 모으기 동안 망치 크기 1 → 이 배율(내려치기 직전에 도달)
|
||
public float shrinkSeconds = 0.15f; // 끝난 뒤 원래 크기로 돌아오는 시간
|
||
public float headFraction = 0.25f; // #913 망치 머리 = 손잡이 축으로 먼 쪽 이 비율의 정점(그 상자 중심 = 모으기 이펙트 · 내려친 지점)
|
||
public float chargeFxDrop = 0.25f; // #914 모으기 이펙트를 망치 머리 중심에서 아래로(m · 망치 배율 곱) — 원형 이펙트가 머리에 거의 가려지게
|
||
public float impactRadius = 3f; // #914 내려친 지점 둘레 이 반경(m · + 적 몸 반지름) 안의 적 = 충격파와 같은 피해 · 0 = 없음
|
||
public int impactLaunch = 1; // #914 1 = 둘레 피해에도 충격파와 같은 띄움 · 기절
|
||
public string glowMaterial = "WL/Fx/WL_Mat_WeaponGlow";
|
||
public float[] glowColor = { 2.2f, 2.2f, 2.2f }; // 흰 빛 색(HDR · 블룸 문턱을 넘으면 번진다)
|
||
public float glowMax = 1f; // 모으기 끝 흰 빛 세기(0~1)
|
||
public float glowFill = 0.6f; // #913 몸통 덮기(낮을수록 망치 외형이 비친다)
|
||
public float glowRimPower = 2f; // #913 테두리 빛 폭(클수록 가장자리만)
|
||
public float glowFadeSeconds = 0.25f; // 내려친 뒤 흰 빛 · 점광원이 꺼지는 시간
|
||
public float[] lightColor = { 0.85f, 0.65f, 1f };
|
||
public float lightRange = 3f;
|
||
public float lightIntensity = 3f; // 0 = 점광원 없음
|
||
public float impactHoldNormalized = 0.46f; // 내려친 자세(망치가 땅에 닿은 프레임)
|
||
public float impactHoldSeconds = 0.5f; // 그 자세로 멈추는 시간
|
||
public int holdBlocksMove = 1; // 1 = 멈춘 동안 이동 입력 무시(구르기는 허용 = 스킵)
|
||
public string note = "";
|
||
}
|
||
|
||
public sealed class SkHammerQuake : PSkill
|
||
{
|
||
public const string ResourcesPath = "WL/WLHammerQuake";
|
||
public static int CastCount, SlamCount, HitCount, AreaHits, Aborted, Holds; public static string LastLog = "";
|
||
static WLHammerQuakeJson s_cfg;
|
||
static readonly Dictionary<string, GameObject> s_fx = new Dictionary<string, GameObject>();
|
||
static Material s_glowMat;
|
||
static readonly int kGlow = Shader.PropertyToID("_Glow"), kGlowColor = Shader.PropertyToID("_GlowColor"), kFill = Shader.PropertyToID("_Fill"), kRim = Shader.PropertyToID("_RimPower");
|
||
|
||
static WLHammerQuakeJson C
|
||
{
|
||
get
|
||
{
|
||
if (s_cfg != null) return s_cfg;
|
||
s_cfg = new WLHammerQuakeJson();
|
||
var ta = Resources.Load<TextAsset>(ResourcesPath);
|
||
if (ta != null) { 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; s_fx.Clear(); s_glowMat = null; }
|
||
|
||
sealed class Wave { public Vector3 o, d; public float t0; public int slam; public GameObject fx; public readonly HashSet<int> hit = new HashSet<int>(); }
|
||
readonly List<Wave> _waves = new List<Wave>();
|
||
readonly Dictionary<int, int> _slamHit = new Dictionary<int, int>();
|
||
Coroutine _co, _tailCo; GameObject _charge; bool _running, _animOwned; int _slam;
|
||
|
||
// #912 망치 연출 — 크기(소켓 원점 = 쥔 손 기준) · 흰 빛 덧그림 · 점광원
|
||
PCActor _pc; Transform _socket; Vector3 _tipLocal, _headLocal; bool _hasTip; float _scale = 1f, _glow, _light;
|
||
readonly List<Transform> _parts = new List<Transform>();
|
||
readonly List<Vector3> _pos0 = new List<Vector3>(), _scl0 = new List<Vector3>();
|
||
readonly List<GameObject> _glowGos = new List<GameObject>();
|
||
readonly List<Renderer> _glowRs = new List<Renderer>();
|
||
MaterialPropertyBlock _mpb; Light _lamp;
|
||
|
||
static PCActor Pc { get { return MyValue.bMyPC ? MyValue.MyPC as PCActor : null; } }
|
||
|
||
public override bool WantCast()
|
||
{
|
||
var pc = Pc;
|
||
if (_running || C.enabled == 0 || pc == null || pc.IsDead() || pc.IsCC() || pc.Get_CurAnim() >= eAnim.Skill) return false;
|
||
return R.AnyEnemyWithin(R.PcPos + R.PcFwd * (C.prongLength * 0.5f), C.prongLength * 0.6f);
|
||
}
|
||
|
||
public override void Cast()
|
||
{
|
||
var pc = Pc;
|
||
if (_running || C.enabled == 0 || pc == null || pc.IsDead() || pc.m_animation == null) return;
|
||
casts++; CastCount++;
|
||
_co = R.StartCoroutine(Run(pc));
|
||
}
|
||
|
||
IEnumerator Run(PCActor pc)
|
||
{
|
||
var c = C; var an = pc.m_animation;
|
||
float impactN = ImpactNormalized(an, c.slamClip);
|
||
if (impactN <= c.holdNormalized) { LastLog = "타격 이벤트 없음 " + c.slamClip; yield break; }
|
||
_running = true; _waves.Clear(); _slamHit.Clear(); _slam = 0;
|
||
pc.Del_Magic();
|
||
pc.Play_Skill(c.slamState); // 원본 — 스킬 상태 · 대상 보기 · 이동 멈춤
|
||
an.fireEvents = false; _animOwned = true; // 1타의 판정 · 이펙트 · 대기 이벤트를 끈다
|
||
an.Play(c.slamState, 0, c.holdNormalized); an.speed = 0f;
|
||
an.Update(0f); // #913 자세를 지금 적용 — 첫 프레임 모으기 이펙트가 이전 자세의 망치 머리에 찍히던 것(실측) 방지
|
||
|
||
// ① 모으기 — 망치가 점점 커지고 하얗게 빛나며 · 망치 끝에 모으기 이펙트 + 점광원
|
||
if (_tailCo != null) { R.StopCoroutine(_tailCo); _tailCo = null; }
|
||
VisualBegin(pc);
|
||
_charge = Spawn(c.chargeFx, Tip(), null, c.chargeFxScale, c.chargeSeconds + 0.6f);
|
||
float t = 0f;
|
||
while (t < c.chargeSeconds)
|
||
{
|
||
if (Broken(pc)) { Abort(pc); yield break; }
|
||
Pin(an, c.slamState, c.holdNormalized);
|
||
float k = Mathf.Clamp01(t / Mathf.Max(0.01f, c.chargeSeconds)), e = 1f - (1f - k) * (1f - k);
|
||
Visual(Mathf.Lerp(1f, c.growScale, e), c.glowMax * k, c.lightIntensity * e);
|
||
t += Time.deltaTime; yield return null;
|
||
}
|
||
Visual(c.growScale, c.glowMax, c.lightIntensity);
|
||
if (_charge != null) { Object.Destroy(_charge); _charge = null; }
|
||
|
||
// ② 내려치기 × slams — 마지막 한 번은 내려친 자세로 멈춘다
|
||
int n = Mathf.Max(1, c.slams);
|
||
for (int k = 0; k < n; k++)
|
||
{
|
||
_slam = k + 1; bool last = k == n - 1;
|
||
an.Play(c.slamState, 0, c.holdNormalized); an.speed = Mathf.Max(0.1f, c.slamSpeed);
|
||
yield return null;
|
||
float sp = Mathf.Max(0.1f, c.slamSpeed);
|
||
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < impactN) { if (Broken(pc)) { Abort(pc); yield break; } if (an.speed != sp) an.speed = sp; Visual(_scale, _glow, _light); yield return null; }
|
||
Impact(pc);
|
||
float endN = last ? c.impactHoldNormalized : c.slamEndNormalized, f0 = Time.time;
|
||
while (an.GetCurrentAnimatorStateInfo(0).normalizedTime < endN) { if (Broken(pc)) { Abort(pc); yield break; } if (an.speed != sp) an.speed = sp; if (last) Fade(f0); else Visual(_scale, _glow, _light); yield return null; }
|
||
if (!last) continue;
|
||
|
||
// ③ 내려친 자세로 멈춤 — 이동 입력 무시(구르기 = 스킵)
|
||
float holdN = an.GetCurrentAnimatorStateInfo(0).normalizedTime;
|
||
an.speed = 0f; Holds++;
|
||
if (c.holdBlocksMove != 0) WL.Combat.Weapon.WLSkillCancel.HoldMove(pc, c.impactHoldSeconds);
|
||
float h = 0f;
|
||
while (h < c.impactHoldSeconds) { if (Broken(pc)) { Abort(pc); yield break; } Pin(an, c.slamState, holdN); Fade(f0); h += Time.deltaTime; yield return null; }
|
||
}
|
||
Release(pc, true, true);
|
||
}
|
||
|
||
void Impact(PCActor pc)
|
||
{
|
||
var c = C;
|
||
var fwd = pc.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize();
|
||
var at = _hasTip ? Tip() : pc.Get_position() + fwd * c.strikeForward;
|
||
at.y = pc.Get_position().y;
|
||
Spawn(c.groundFx, at, Quaternion.LookRotation(fwd), c.groundFxScale, c.groundFxLife);
|
||
// #914 내려친 지점 둘레 impactRadius m 안의 적 = 충격파와 같은 피해(+ 띄움 · 기절) · 같은 내려치기의 충격파와 겹쳐도 적 1 회
|
||
int area = 0;
|
||
if (c.impactRadius > 0f)
|
||
for (int e = 0; e < R.Enemies.Count; e++)
|
||
{
|
||
var a = R.Enemies[e];
|
||
if (!WLPartSkillCombat.Valid(a)) continue;
|
||
var v = a.Get_position() - at; v.y = 0f;
|
||
if (v.magnitude > c.impactRadius + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||
if (Hit(a, _slam, c.impactLaunch != 0)) area++;
|
||
}
|
||
AreaHits += area;
|
||
float pathScale = c.prongLength / Mathf.Max(0.1f, c.pathFxNativeLength);
|
||
foreach (var ang in c.prongAngles ?? new float[0])
|
||
{
|
||
var d = Quaternion.Euler(0f, ang, 0f) * fwd;
|
||
var w = new Wave { o = at, d = d, t0 = Time.time, slam = _slam };
|
||
w.fx = Spawn(c.pathFx, at, Quaternion.LookRotation(d), pathScale, c.pathFxLife);
|
||
_waves.Add(w);
|
||
}
|
||
SlamCount++;
|
||
LastLog = "내려치기 " + _slam + "/" + c.slams + " · 충격파 " + (c.prongAngles != null ? c.prongAngles.Length : 0) + " 갈래 · 둘레 " + c.impactRadius + " m 명중 " + area + " · 지점 " + at.ToString("F1") + " · 망치 " + _scale.ToString("F2") + " 배";
|
||
}
|
||
|
||
/// <summary>한 번 내려칠 때 적 1 회 — 피해(레벨표 dmg_pct) · launch 면 띄움 · 기절(보스 제외).</summary>
|
||
bool Hit(Actor a, int slam, bool launch)
|
||
{
|
||
int id = a.GetInstanceID();
|
||
int last; if (_slamHit.TryGetValue(id, out last) && last == slam) return false;
|
||
_slamHit[id] = slam;
|
||
if (!WLPartSkillCombat.Damage(a, Dmg(row != null ? row.dmgPct : 100f), true, Key)) return false;
|
||
HitCount++;
|
||
WL.Combat.Impact.WLImpactFx.PlaySkillHitByPC(a, "hammer_quake"); // #936 맞은 적 몸 중심에 해머 스킬 피격 이펙트(WLImpactFx.json skillHitFx 10501 · 투사체가 없는 WL 판정이라 직접 부른다)
|
||
var c = C;
|
||
if (launch && !a.IsSubRole(eSubRol.Boss)) WL.Combat.Weapon.WLLaunch.Apply(a, c.launchHeight, c.launchSeconds, c.stunSeconds);
|
||
return true;
|
||
}
|
||
|
||
public override void Tick(float dt)
|
||
{
|
||
if (_waves.Count == 0) return;
|
||
var c = C; float now = Time.time;
|
||
for (int i = _waves.Count - 1; i >= 0; i--)
|
||
{
|
||
var w = _waves[i];
|
||
float age = now - w.t0;
|
||
float front = Mathf.Min(c.prongLength, age / Mathf.Max(0.05f, c.prongTravelSeconds) * c.prongLength);
|
||
for (int e = 0; e < R.Enemies.Count; e++)
|
||
{
|
||
var a = R.Enemies[e];
|
||
if (!WLPartSkillCombat.Valid(a)) continue;
|
||
var v = a.Get_position() - w.o; v.y = 0f;
|
||
float along = Vector3.Dot(v, w.d);
|
||
if (along < -0.3f || along > front) continue;
|
||
float perp = (v - w.d * along).magnitude;
|
||
if (perp > c.prongWidth * 0.5f + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||
if (!w.hit.Add(a.GetInstanceID())) continue;
|
||
Hit(a, w.slam, true); // 한 번 내려칠 때 적 1 회(둘레 피해와도)
|
||
}
|
||
if (age > c.prongTravelSeconds + 0.05f) _waves.RemoveAt(i);
|
||
}
|
||
}
|
||
|
||
bool Broken(PCActor pc) { return pc == null || pc.IsDead() || pc.Get_CurAnim() != eAnim.Skill; }
|
||
|
||
/// <summary>그 자세로 멈춤 유지 — 다른 연출(돌진 끝 · 공격 배속 등)이 애니메이터 속도를 되돌리면 다시 멈춘다(#912 실측: 모으기 중 속도 1.0 · n 0.38 로 풀림).</summary>
|
||
static void Pin(Animator an, string state, float n)
|
||
{
|
||
if (an.speed != 0f) an.speed = 0f;
|
||
if (Mathf.Abs(an.GetCurrentAnimatorStateInfo(0).normalizedTime - n) > 0.01f) an.Play(state, 0, n);
|
||
}
|
||
|
||
/// <summary>이동 · 구르기로 끊김 = 스킵 — 남은 판정 · 이펙트를 바로 멈춘다(애니메이터는 입력이 이미 넘겨받았다).</summary>
|
||
void Abort(PCActor pc)
|
||
{
|
||
Aborted++; LastLog = "끊김(이동 · 구르기) — 내려치기 " + _slam;
|
||
foreach (var w in _waves) if (w.fx != null) Object.Destroy(w.fx);
|
||
_waves.Clear();
|
||
Release(pc, false, true);
|
||
}
|
||
|
||
/// <summary>끝 — 애니메이터 되돌림 · 대기(toIdle) · 멈춤 동안 미룬 뗌 적용 · 망치는 tail 이면 shrinkSeconds 동안 원래 크기(아니면 즉시).</summary>
|
||
void Release(PCActor pc, bool toIdle, bool tail)
|
||
{
|
||
if (_charge != null) { Object.Destroy(_charge); _charge = null; }
|
||
var an = pc != null ? pc.m_animation : null;
|
||
if (_animOwned && an != null) { an.speed = 1f; an.fireEvents = true; }
|
||
_animOwned = false; _running = false; _co = null;
|
||
if (toIdle && pc != null && !pc.IsDead() && pc.Get_CurAnim() == eAnim.Skill) pc.Play_Idle();
|
||
WL.Combat.Weapon.WLSkillCancel.EndHoldMove(pc, toIdle);
|
||
if (_tailCo != null && R != null) { R.StopCoroutine(_tailCo); _tailCo = null; }
|
||
if (tail && R != null && R.isActiveAndEnabled && (_parts.Count > 0 || _glowGos.Count > 0 || _lamp != null)) _tailCo = R.StartCoroutine(Shrink());
|
||
else VisualEnd();
|
||
}
|
||
|
||
public override void Dispose()
|
||
{
|
||
if (_co != null && R != null) R.StopCoroutine(_co);
|
||
if (_running) Release(Pc, false, false);
|
||
if (_tailCo != null && R != null) R.StopCoroutine(_tailCo);
|
||
_tailCo = null; VisualEnd();
|
||
foreach (var w in _waves) if (w.fx != null) Object.Destroy(w.fx);
|
||
_waves.Clear();
|
||
}
|
||
|
||
public override string State { get { return "running=" + _running + " slam=" + _slam + " waves=" + _waves.Count + " hits=" + HitCount + " scale=" + _scale.ToString("F2") + " glow=" + _glow.ToString("F2"); } }
|
||
|
||
// ── #912 망치 연출 ─────────────────────────────────────────────────────
|
||
|
||
/// <summary>망치 찾기(WLImpactRunner.SocketTip = 가장 먼 렌더러 꼭짓점) · 소켓 자식 크기 기록 · 흰 빛 덧그림(메시 복제 자식) · 점광원.</summary>
|
||
void VisualBegin(PCActor pc)
|
||
{
|
||
VisualEnd();
|
||
var c = C; _pc = pc;
|
||
_hasTip = WL.Combat.Impact.WLImpactRunner.SocketTip(pc, out _socket, out _tipLocal);
|
||
if (_hasTip)
|
||
{
|
||
_headLocal = HeadCenter(_socket, _tipLocal, c.headFraction);
|
||
foreach (Transform ch in _socket) { if (!ch.gameObject.activeSelf) continue; _parts.Add(ch); _pos0.Add(ch.localPosition); _scl0.Add(ch.localScale); }
|
||
if (s_glowMat == null && !string.IsNullOrEmpty(c.glowMaterial)) s_glowMat = Resources.Load<Material>(c.glowMaterial);
|
||
if (s_glowMat == null) LastLog = "흰 빛 재질 없음 " + c.glowMaterial;
|
||
else
|
||
foreach (var mf in _socket.GetComponentsInChildren<MeshFilter>(false))
|
||
{
|
||
var mr = mf.GetComponent<MeshRenderer>();
|
||
if (mr == null || !mr.enabled || mf.sharedMesh == null) continue;
|
||
var go = new GameObject("[WL912] Glow") { layer = mf.gameObject.layer };
|
||
go.transform.SetParent(mf.transform, false);
|
||
go.AddComponent<MeshFilter>().sharedMesh = mf.sharedMesh;
|
||
var r = go.AddComponent<MeshRenderer>();
|
||
var mats = new Material[Mathf.Max(1, mf.sharedMesh.subMeshCount)];
|
||
for (int i = 0; i < mats.Length; i++) mats[i] = s_glowMat;
|
||
r.sharedMaterials = mats; r.enabled = false;
|
||
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; r.receiveShadows = false;
|
||
r.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; r.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
|
||
_glowGos.Add(go); _glowRs.Add(r);
|
||
}
|
||
}
|
||
if (c.lightIntensity > 0f)
|
||
{
|
||
_lamp = new GameObject("[WL912] ChargeLight").AddComponent<Light>();
|
||
_lamp.type = LightType.Point; _lamp.range = Mathf.Max(0.1f, c.lightRange); _lamp.color = Rgb(c.lightColor, 1f);
|
||
_lamp.shadows = LightShadows.None; _lamp.intensity = 0f; _lamp.enabled = false;
|
||
}
|
||
_scale = 1f; _glow = 0f; _light = 0f;
|
||
}
|
||
|
||
/// <summary>망치 크기(소켓 원점 기준 — 위치 · 크기 함께 배율) · 흰 빛 세기 · 점광원 세기를 쓰고 · 점광원 · 모으기 이펙트를 망치 끝에 둔다.</summary>
|
||
void Visual(float scale, float glow, float light)
|
||
{
|
||
_scale = scale; _glow = glow; _light = light;
|
||
for (int i = 0; i < _parts.Count; i++) { var p = _parts[i]; if (p == null) continue; p.localPosition = _pos0[i] * scale; p.localScale = _scl0[i] * scale; }
|
||
if (_glowRs.Count > 0)
|
||
{
|
||
if (_mpb == null) _mpb = new MaterialPropertyBlock();
|
||
var c = C; var col = Rgb(c.glowColor, 2f);
|
||
for (int i = 0; i < _glowRs.Count; i++)
|
||
{
|
||
var r = _glowRs[i]; if (r == null) continue;
|
||
r.enabled = glow > 0.001f;
|
||
_mpb.Clear(); _mpb.SetFloat(kGlow, glow); _mpb.SetColor(kGlowColor, col); _mpb.SetFloat(kFill, c.glowFill); _mpb.SetFloat(kRim, c.glowRimPower);
|
||
r.SetPropertyBlock(_mpb);
|
||
}
|
||
}
|
||
var tip = Tip();
|
||
if (_lamp != null) { _lamp.transform.position = tip; _lamp.intensity = light; _lamp.enabled = light > 0.01f; }
|
||
if (_charge != null) _charge.transform.position = tip + Vector3.down * (C.chargeFxDrop * scale); // #914 머리에 거의 가려지게 아래로
|
||
}
|
||
|
||
void Fade(float since)
|
||
{
|
||
var c = C; float k = 1f - Mathf.Clamp01((Time.time - since) / Mathf.Max(0.01f, c.glowFadeSeconds));
|
||
Visual(_scale, c.glowMax * k, c.lightIntensity * k);
|
||
}
|
||
|
||
IEnumerator Shrink()
|
||
{
|
||
float s0 = _scale, g0 = _glow, l0 = _light, dur = Mathf.Max(0.01f, C.shrinkSeconds), t = 0f;
|
||
while (t < dur) { t += Time.deltaTime; float k = Mathf.Clamp01(t / dur); Visual(Mathf.Lerp(s0, 1f, k), g0 * (1f - k), l0 * (1f - k)); yield return null; }
|
||
_tailCo = null; VisualEnd();
|
||
}
|
||
|
||
void VisualEnd()
|
||
{
|
||
for (int i = 0; i < _parts.Count; i++) { var p = _parts[i]; if (p == null) continue; p.localPosition = _pos0[i]; p.localScale = _scl0[i]; }
|
||
_parts.Clear(); _pos0.Clear(); _scl0.Clear();
|
||
for (int i = 0; i < _glowGos.Count; i++) if (_glowGos[i] != null) Object.Destroy(_glowGos[i]);
|
||
_glowGos.Clear(); _glowRs.Clear();
|
||
if (_lamp != null) Object.Destroy(_lamp.gameObject);
|
||
_lamp = null; _scale = 1f; _glow = 0f; _light = 0f;
|
||
}
|
||
|
||
/// <summary>망치 머리 중심(커진 크기 반영) — 모으기 이펙트 · 내려친 지점. 망치가 없으면 PC 가슴 높이.</summary>
|
||
Vector3 Tip()
|
||
{
|
||
if (_hasTip && _socket != null) return _socket.TransformPoint(_headLocal * _scale);
|
||
return _pc != null ? _pc.Get_position() + Vector3.up : Vector3.zero;
|
||
}
|
||
|
||
/// <summary>#913 망치 머리 중심(소켓 로컬) — 소켓 아래 메시마다 가장 긴 축을 손잡이 축으로(쥔 손에서 먼 쪽이 머리) 보고, 그 축으로 먼 쪽 frac 비율 안의
|
||
/// 정점 상자 중심. 가장 긴 메시(= 망치)를 쓴다 · 정점을 못 읽으면 가장 먼 꼭짓점(tipLocal).</summary>
|
||
static Vector3 HeadCenter(Transform socket, Vector3 tipLocal, float frac)
|
||
{
|
||
Vector3 best = tipLocal; float bestLen = -1f;
|
||
foreach (var mf in socket.GetComponentsInChildren<MeshFilter>(false))
|
||
{
|
||
var mr = mf.GetComponent<MeshRenderer>(); var mesh = mf.sharedMesh;
|
||
if (mr == null || !mr.enabled || mesh == null || !mesh.isReadable) continue;
|
||
var M = socket.worldToLocalMatrix * mf.transform.localToWorldMatrix;
|
||
var b = mesh.bounds; var e = b.extents;
|
||
int ax = e.x >= e.y && e.x >= e.z ? 0 : (e.y >= e.z ? 1 : 2);
|
||
var dirL = ax == 0 ? Vector3.right : ax == 1 ? Vector3.up : Vector3.forward;
|
||
var dir = M.MultiplyVector(dirL).normalized;
|
||
if (M.MultiplyPoint3x4(b.center - dirL * e[ax]).sqrMagnitude > M.MultiplyPoint3x4(b.center + dirL * e[ax]).sqrMagnitude) dir = -dir;
|
||
var verts = mesh.vertices; if (verts.Length == 0) continue;
|
||
float mn = float.MaxValue, mx = float.MinValue;
|
||
for (int i = 0; i < verts.Length; i++) { float pr = Vector3.Dot(M.MultiplyPoint3x4(verts[i]), dir); if (pr < mn) mn = pr; if (pr > mx) mx = pr; }
|
||
float cut = mx - Mathf.Clamp01(frac) * (mx - mn);
|
||
Vector3 lo = Vector3.one * float.MaxValue, hi = Vector3.one * float.MinValue; int n = 0;
|
||
for (int i = 0; i < verts.Length; i++)
|
||
{
|
||
var p = M.MultiplyPoint3x4(verts[i]);
|
||
if (Vector3.Dot(p, dir) < cut) continue;
|
||
lo = Vector3.Min(lo, p); hi = Vector3.Max(hi, p); n++;
|
||
}
|
||
if (n > 0 && mx - mn > bestLen) { bestLen = mx - mn; best = (lo + hi) * 0.5f; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
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); }
|
||
|
||
static float ImpactNormalized(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, Quaternion? rot, 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) { LastLog = "이펙트 없음 " + path; return null; }
|
||
var go = Object.Instantiate(prefab, pos, rot.HasValue ? rot.Value * prefab.transform.rotation : prefab.transform.rotation);
|
||
go.transform.localScale = prefab.transform.localScale * Mathf.Max(0.01f, scale);
|
||
Object.Destroy(go, Mathf.Max(0.1f, life));
|
||
return go;
|
||
}
|
||
}
|
||
}
|