2026-10-20 04:42:05 +00:00
// ─────────────────────────────────────────────────────────────────────
2026-10-20 05:05:52 +00:00
// WLWeaponTipGlow.cs — 무기 끝 은은한 빛 (#948 · 2026-10-20 · 2차: 쿼드 → 팩 이펙트 · 끝 = 축 위 끝점)
2026-10-20 04:42:05 +00:00
/ /
// PD 원문: 「마법검의 지팡이 끝은 은은하게 빛나도록 해줘. (마법 무기라는 느낌이 들도록)」
2026-10-20 05:05:52 +00:00
// 「지팡이 끝에 알 수 없는 흰색 선이 생겨났고 광원의 위치가 지팡이의 둥근 면이 아니라 엉뚱한 위치」
// ■ 1차 실패: 무기 자식 쿼드가 무기의 비균등 스케일에 눌려 흰 선이 됐고, 끝 = 경계 상자 모서리라 축에서 벗어났다.
// ■ 2차: 끝 = 소켓 → 렌더러 경계 중심 방향(= 무기 축) 위에서 가장 먼 모서리의 투영점(둥근 끝면 중심) · 빛 = 씬 루트의 홀더가 매 프레임 그 점을 따라감 ·
// 겉모습 = 기존 팩 이펙트 프리팹(기본 WL_Fx_BowCharge · 시전 모으기와 같은 것)을 루프로 켜 둔다 + 약한 점광원 · 사인 맥동(홀더 스케일)
// ■ 붙는 자리 = WLWeaponFitter 손 맞춤 뒤 훅 1줄(WLOrbFloat 과 같은 자리) · 값 = Resources/WL/WLWeaponTipGlow.json (C45) · enabled 0 = 아무것도 안 함
2026-10-20 04:42:05 +00:00
// 🔴 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 ] ; // 또는 무기 인스턴스 이름 앞부분
2026-10-20 05:05:52 +00:00
public string prefab = "WL/Fx/WL_Fx_BowCharge" ; // Resources 경로 · 파티클을 전부 루프로
public float scale = 0.06f ;
public float pulse = 0.2f ; // 맥동 진폭(크기 ±비율)
public float pulseSeconds = 1.6f ;
2026-10-20 04:42:05 +00:00
public int light = 1 ; // 1 = 점광원도
2026-10-20 05:05:52 +00:00
public float [ ] lightColor = { 1f , 0.8f , 0.45f , 1f } ;
public float lightRange = 0.8f ;
public float lightIntensity = 0.3f ;
public float tipPull = 0f ; // 끝에서 손 쪽으로 당길 거리(m · 둥근 면 안쪽에 두고 싶을 때)
2026-10-21 09:37:16 +00:00
public int tipFromMesh = 0 ; // #963 1 = 끝 = 무기 메시 로컬 경계의 긴 축에서 손잡이(소켓)와 먼 쪽 면 중심 − tipPull(자세 · 월드 축 정렬 상자 부풀림과 무관 · 이전 = 붙는 순간의 월드 상자 모서리 투영이라 머리 위로 0.1 m 벗어남 실측)
2026-10-20 07:32:41 +00:00
public float cameraPull = 0f ; // #948-5 끝점에서 카메라 쪽으로 당길 거리(m) — 둥근 머리가 카메라와 구슬 사이에 올 때(휘두르는 동작) 구슬이 가려져 주변 빛만 보이는 것을 막는다
2026-10-20 04:42:05 +00:00
}
[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 재읽기)
2026-10-20 05:05:52 +00:00
Transform _weapon , _socket , _holder ; Light _light ; WLWeaponTipGlowRule _r ; float _t0 ; Vector3 _localTip ;
2026-10-20 04:42:05 +00:00
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 ;
2026-10-21 09:37:16 +00:00
if ( ! HasMesh ( weapon ) ) return false ; // #963 메시 없는 물체(공격 중 손 소켓에 붙는 WL_Fx_BowCharge 같은 이펙트)엔 붙이지 않는다 — 그 원점에 빛이 하나 더 떴다(Play 실측)
2026-10-20 04:42:05 +00:00
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 ( ) ;
2026-10-20 05:05:52 +00:00
Attached + + ; LastLog = "무기 끝 빛 " + weapon . name + " 직업 " + cls + " 끝(로컬) " + g . _localTip . ToString ( "F2" ) + ( g . _holder ! = null ? " · " + rule . prefab : " · 프리팹 없음" ) ;
2026-10-20 04:42:05 +00:00
return true ;
}
catch ( System . Exception ex ) { LastLog = "무기 끝 빛 실패 — " + ex . Message ; return false ; }
}
2026-10-21 09:37:16 +00:00
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 ) ; }
2026-10-20 05:05:52 +00:00
/// <summary>무기 끝 = 소켓에서 렌더러 경계 중심을 향한 축 위, 가장 멀리 투영되는 모서리의 투영점(둥근 끝면 중심 · 상자 모서리가 아님).</summary>
2026-10-20 04:42:05 +00:00
void Build ( )
{
var r = _r ;
2026-10-20 05:05:52 +00:00
bool any = false ; Bounds b = new Bounds ( _weapon . position , Vector3 . zero ) ;
2026-10-20 04:42:05 +00:00
foreach ( var rd in _weapon . GetComponentsInChildren < Renderer > ( false ) )
{
if ( rd is ParticleSystemRenderer | | ! rd . enabled ) continue ;
2026-10-20 05:05:52 +00:00
if ( ! any ) { b = rd . bounds ; any = true ; } else b . Encapsulate ( rd . bounds ) ;
}
2026-10-21 09:37:16 +00:00
Vector3 tip ; Vector3 ml ;
if ( r . tipFromMesh ! = 0 & & MeshTip ( out ml ) ) tip = _weapon . TransformPoint ( ml ) ; // #963
else if ( ! any ) tip = _weapon . position ;
2026-10-20 05:05:52 +00:00
else
{
Vector3 o = _socket . position ; Vector3 axis = b . center - o ; if ( axis . sqrMagnitude < 1e-6f ) axis = _weapon . up ; axis . Normalize ( ) ;
float far = 0f ;
2026-10-20 04:42:05 +00:00
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 ) ;
2026-10-20 05:05:52 +00:00
float d = Vector3 . Dot ( corner - o , axis ) ; if ( d > far ) far = d ;
2026-10-20 04:42:05 +00:00
}
2026-10-20 05:05:52 +00:00
tip = o + axis * Mathf . Max ( 0f , far - r . tipPull ) ;
2026-10-20 04:42:05 +00:00
}
2026-10-20 05:05:52 +00:00
_localTip = _weapon . InverseTransformPoint ( tip ) ;
2026-10-20 04:42:05 +00:00
2026-10-20 05:05:52 +00:00
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 ;
}
2026-10-20 04:42:05 +00:00
if ( r . light ! = 0 )
{
2026-10-20 05:05:52 +00:00
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 ) ;
2026-10-20 04:42:05 +00:00
}
}
void LateUpdate ( )
{
2026-10-20 05:05:52 +00:00
if ( _holder = = null | | _weapon = = null ) return ;
2026-10-20 04:42:05 +00:00
float k = 1f + ( _r . pulse > 0f & & _r . pulseSeconds > 0f ? _r . pulse * Mathf . Sin ( ( Time . time - _t0 ) / _r . pulseSeconds * Mathf . PI * 2f ) : 0f ) ;
2026-10-20 07:32:41 +00:00
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 ;
2026-10-20 05:05:52 +00:00
_holder . localScale = Vector3 . one * k ;
2026-10-20 04:42:05 +00:00
if ( _light ! = null ) _light . intensity = _r . lightIntensity * k ;
}
2026-10-20 05:05:52 +00:00
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 ) ; }
2026-10-20 04:42:05 +00:00
}
}