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

187 lines
12 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.

// ─────────────────────────────────────────────────────────────────────
// WLWeaponTipGlow.cs — 무기 끝 은은한 빛 (#948 · 2026-10-20 · 2차: 쿼드 → 팩 이펙트 · 끝 = 축 위 끝점)
//
// PD 원문: 「마법검의 지팡이 끝은 은은하게 빛나도록 해줘. (마법 무기라는 느낌이 들도록)」
// 「지팡이 끝에 알 수 없는 흰색 선이 생겨났고 광원의 위치가 지팡이의 둥근 면이 아니라 엉뚱한 위치」
// ■ 1차 실패: 무기 자식 쿼드가 무기의 비균등 스케일에 눌려 흰 선이 됐고, 끝 = 경계 상자 모서리라 축에서 벗어났다.
// ■ 2차: 끝 = 소켓 → 렌더러 경계 중심 방향(= 무기 축) 위에서 가장 먼 모서리의 투영점(둥근 끝면 중심) · 빛 = 씬 루트의 홀더가 매 프레임 그 점을 따라감 ·
// 겉모습 = 기존 팩 이펙트 프리팹(기본 WL_Fx_BowCharge · 시전 모으기와 같은 것)을 루프로 켜 둔다 + 약한 점광원 · 사인 맥동(홀더 스케일)
// ■ 붙는 자리 = WLWeaponFitter 손 맞춤 뒤 훅 1줄(WLOrbFloat 과 같은 자리) · 값 = Resources/WL/WLWeaponTipGlow.json (C45) · enabled 0 = 아무것도 안 함
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업) — 예외는 LastLog 로만
// ─────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Weapon
{
[System.Serializable]
public class WLWeaponTipGlowRule
{
public int[] classIds = new int[0]; // 이 직업의 무기에 붙인다(PCActor.Get_ID)
public string[] weaponNames = new string[0]; // 또는 무기 인스턴스 이름 앞부분
public string prefab = "WL/Fx/WL_Fx_BowCharge"; // Resources 경로 · 파티클을 전부 루프로
public float scale = 0.06f;
public float pulse = 0.2f; // 맥동 진폭(크기 ±비율)
public float pulseSeconds = 1.6f;
public int light = 1; // 1 = 점광원도
public float[] lightColor = { 1f, 0.8f, 0.45f, 1f };
public float lightRange = 0.8f;
public float lightIntensity = 0.3f;
public float tipPull = 0f; // 끝에서 손 쪽으로 당길 거리(m · 둥근 면 안쪽에 두고 싶을 때)
public int tipFromMesh = 0; // #963 1 = 끝 = 무기 메시 로컬 경계의 긴 축에서 손잡이(소켓)와 먼 쪽 면 중심 − tipPull(자세 · 월드 축 정렬 상자 부풀림과 무관 · 이전 = 붙는 순간의 월드 상자 모서리 투영이라 머리 위로 0.1 m 벗어남 실측)
public float cameraPull = 0f; // #948-5 끝점에서 카메라 쪽으로 당길 거리(m) — 둥근 머리가 카메라와 구슬 사이에 올 때(휘두르는 동작) 구슬이 가려져 주변 빛만 보이는 것을 막는다
}
[System.Serializable]
public class WLWeaponTipGlowJson { public int enabled = 1; public WLWeaponTipGlowRule[] rules = new WLWeaponTipGlowRule[0]; public string note = ""; }
public sealed class WLWeaponTipGlow : MonoBehaviour
{
public const string ResourcesPath = "WL/WLWeaponTipGlow";
public static int Attached; public static string LastLog = "";
static WLWeaponTipGlowJson s_cfg; static bool s_loaded;
static WLWeaponTipGlowJson Cfg
{
get
{
if (!s_loaded)
{
s_loaded = true;
var ta = Resources.Load<TextAsset>(ResourcesPath);
try { s_cfg = ta != null ? JsonUtility.FromJson<WLWeaponTipGlowJson>(ta.text) : null; } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
}
return s_cfg;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() { s_loaded = false; s_cfg = null; } // 도메인 리로드 끔 대비(JSON 재읽기)
Transform _weapon, _socket, _holder; Light _light; WLWeaponTipGlowRule _r; float _t0; Vector3 _localTip;
public static bool TryAttach(Transform weapon)
{
try
{
var c = Cfg; if (c == null || c.enabled == 0 || weapon == null || c.rules == null) return false;
if (weapon.GetComponent<WLWeaponTipGlow>() != null) return true;
var pc = weapon.GetComponentInParent<PCActor>(); int cls = pc != null ? pc.Get_ID() : 0;
WLWeaponTipGlowRule rule = null;
foreach (var r in c.rules)
{
if (r == null) continue;
bool byClass = r.classIds != null && System.Array.IndexOf(r.classIds, cls) >= 0;
bool byName = false;
if (r.weaponNames != null) foreach (var n in r.weaponNames) if (!string.IsNullOrEmpty(n) && weapon.name.StartsWith(n)) { byName = true; break; }
if (byClass || byName) { rule = r; break; }
}
if (rule == null) return false;
if (!HasMesh(weapon)) return false; // #963 메시 없는 물체(공격 중 손 소켓에 붙는 WL_Fx_BowCharge 같은 이펙트)엔 붙이지 않는다 — 그 원점에 빛이 하나 더 떴다(Play 실측)
var g = weapon.gameObject.AddComponent<WLWeaponTipGlow>();
g._weapon = weapon; g._socket = weapon.parent != null ? weapon.parent : weapon; g._r = rule; g._t0 = Time.time;
g.Build();
Attached++; LastLog = "무기 끝 빛 " + weapon.name + " 직업 " + cls + " 끝(로컬) " + g._localTip.ToString("F2") + (g._holder != null ? " · " + rule.prefab : " · 프리팹 없음");
return true;
}
catch (System.Exception ex) { LastLog = "무기 끝 빛 실패 — " + ex.Message; return false; }
}
static bool HasMesh(Transform w)
{
foreach (var mf in w.GetComponentsInChildren<MeshFilter>(true)) if (mf.sharedMesh != null && mf.GetComponent<Renderer>() != null) return true;
foreach (var sm in w.GetComponentsInChildren<SkinnedMeshRenderer>(true)) if (sm.sharedMesh != null) return true;
return false;
}
/// <summary>#963 무기 머리(로컬) — 무기 로컬 기준 메시 경계(MeshFilter · SkinnedMeshRenderer)의 긴 축에서 손잡이(socket)와 먼 쪽 면 중심 − pull(m). 자세 · 월드 축 정렬 상자 부풀림과 무관. WLStaffEarth 모으기도 같은 점을 쓴다.</summary>
public static bool HeadLocal(Transform weapon, Transform socket, float pull, out Vector3 local)
{
local = Vector3.zero; if (weapon == null) return false; bool any = false; Vector3 mn = Vector3.zero, mx = Vector3.zero;
System.Action<Bounds, Transform> add = (b, tr) =>
{
for (int i = 0; i < 8; i++)
{
var corner = new Vector3((i & 1) == 0 ? b.min.x : b.max.x, (i & 2) == 0 ? b.min.y : b.max.y, (i & 4) == 0 ? b.min.z : b.max.z);
var l = weapon.InverseTransformPoint(tr.TransformPoint(corner));
if (!any) { mn = mx = l; any = true; } else { mn = Vector3.Min(mn, l); mx = Vector3.Max(mx, l); }
}
};
foreach (var mf in weapon.GetComponentsInChildren<MeshFilter>(false)) if (mf.sharedMesh != null && mf.GetComponent<Renderer>() != null) add(mf.sharedMesh.bounds, mf.transform);
foreach (var sm in weapon.GetComponentsInChildren<SkinnedMeshRenderer>(false)) if (sm.sharedMesh != null) add(sm.localBounds, sm.transform);
if (!any) return false;
var size = mx - mn; int ax = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2);
Vector3 c = (mn + mx) * 0.5f, hi = c, lo = c; hi[ax] = mx[ax]; lo[ax] = mn[ax];
Vector3 grip = socket != null ? weapon.InverseTransformPoint(socket.position) : Vector3.zero;
bool hiFar = Mathf.Abs(hi[ax] - grip[ax]) >= Mathf.Abs(lo[ax] - grip[ax]);
Vector3 tip = hiFar ? hi : lo;
float s = Mathf.Max(1e-4f, Mathf.Abs(weapon.lossyScale[ax]));
tip[ax] += (hiFar ? -1f : 1f) * pull / s;
local = tip; return true;
}
bool MeshTip(out Vector3 local) { return HeadLocal(_weapon, _socket, _r.tipPull, out local); }
/// <summary>무기 끝 = 소켓에서 렌더러 경계 중심을 향한 축 위, 가장 멀리 투영되는 모서리의 투영점(둥근 끝면 중심 · 상자 모서리가 아님).</summary>
void Build()
{
var r = _r;
bool any = false; Bounds b = new Bounds(_weapon.position, Vector3.zero);
foreach (var rd in _weapon.GetComponentsInChildren<Renderer>(false))
{
if (rd is ParticleSystemRenderer || !rd.enabled) continue;
if (!any) { b = rd.bounds; any = true; } else b.Encapsulate(rd.bounds);
}
Vector3 tip; Vector3 ml;
if (r.tipFromMesh != 0 && MeshTip(out ml)) tip = _weapon.TransformPoint(ml); // #963
else if (!any) tip = _weapon.position;
else
{
Vector3 o = _socket.position; Vector3 axis = b.center - o; if (axis.sqrMagnitude < 1e-6f) axis = _weapon.up; axis.Normalize();
float far = 0f;
for (int i = 0; i < 8; i++)
{
var corner = new Vector3((i & 1) == 0 ? b.min.x : b.max.x, (i & 2) == 0 ? b.min.y : b.max.y, (i & 4) == 0 ? b.min.z : b.max.z);
float d = Vector3.Dot(corner - o, axis); if (d > far) far = d;
}
tip = o + axis * Mathf.Max(0f, far - r.tipPull);
}
_localTip = _weapon.InverseTransformPoint(tip);
var holder = new GameObject("~WL_TipGlow"); _holder = holder.transform; // 씬 루트(무기 스케일 영향 0)
_holder.position = tip;
var p = Resources.Load<GameObject>(r.prefab);
if (p != null)
{
var fx = Instantiate(p, _holder); fx.transform.localPosition = Vector3.zero; fx.transform.localRotation = Quaternion.identity; fx.transform.localScale = Vector3.one * r.scale;
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.loop = true; m.playOnAwake = true; ps.Play(true); }
var fade = fx.GetComponent<MonoBehaviour>(); // 사본 프리팹에 수명 스크립트(TurnOff_GO · WLFxFade)가 있으면 끈다 — 계속 켜 둬야 한다
foreach (var mb in fx.GetComponents<MonoBehaviour>()) if (mb != null && !(mb is ParticleSystem)) mb.enabled = false;
}
if (r.light != 0)
{
var lg = new GameObject("~WL_TipLight"); lg.transform.SetParent(_holder, false);
_light = lg.AddComponent<Light>(); _light.type = LightType.Point; _light.range = r.lightRange; _light.intensity = r.lightIntensity; _light.shadows = LightShadows.None;
_light.color = r.lightColor != null && r.lightColor.Length >= 3 ? new Color(r.lightColor[0], r.lightColor[1], r.lightColor[2]) : new Color(1f, 0.8f, 0.45f);
}
}
void LateUpdate()
{
if (_holder == null || _weapon == null) return;
float k = 1f + (_r.pulse > 0f && _r.pulseSeconds > 0f ? _r.pulse * Mathf.Sin((Time.time - _t0) / _r.pulseSeconds * Mathf.PI * 2f) : 0f);
var tipW = _weapon.TransformPoint(_localTip);
if (_r.cameraPull > 0f)
{
var cam = Camera.main;
if (cam != null) { var toCam = cam.transform.position - tipW; if (toCam.sqrMagnitude > 1e-4f) tipW += toCam.normalized * _r.cameraPull; } // 머리 메시 앞으로 (가림 방지)
}
_holder.position = tipW;
_holder.localScale = Vector3.one * k;
if (_light != null) _light.intensity = _r.lightIntensity * k;
}
void OnDisable() { if (_holder != null) _holder.gameObject.SetActive(false); }
void OnEnable() { if (_holder != null) _holder.gameObject.SetActive(true); }
void OnDestroy() { if (_holder != null) Destroy(_holder.gameObject); }
}
}