150 lines
9.4 KiB
C#
150 lines
9.4 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 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;
|
|
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; }
|
|
}
|
|
|
|
/// <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;
|
|
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); }
|
|
}
|
|
}
|