187 lines
12 KiB
C#
187 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────
|
||
// 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); }
|
||
}
|
||
}
|