172 lines
9.8 KiB
C#
172 lines
9.8 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLEquipPreview.cs — 장비 화면 3D 캐릭터 미리보기 (2026-10-12 · 레퍼런스 「캐릭터 중심 장비 화면」)
|
||
//
|
||
// 모델 = 코스튬 프리팹을 `WLCharacterSwapSettings.Resolve` 로 바꾼 이름(실제 PC 생성 PCInfo.cs:16 과 같은 규칙 = 붉은 기사 LH_M05).
|
||
// 힘민지 원본 `UI3DModel`(EquipmentUI 의 PC 모델 로더 · Addressables PC 프리팹 + Stop + Set_UIPC + 물리 제거)을 그대로 상속하고,
|
||
// 원본은 화면 카메라(depth 6 · 직교)로 바로 그리지만 WL 장비 GUI 는 ScreenSpaceOverlay 캔버스라 **RenderTexture 카메라**로 그려 RawImage 에 띄운다.
|
||
// 리그는 월드 밖(hub.rigY · 기본 −2000)에 두고 카메라는 "UI" 레이어만 본다(UI3DModel 이 모델 전체를 UI 레이어로 바꾼다).
|
||
// 장착 무기 → 직업(WLWeaponAttackTable.ClassIdFor)으로 무기 모델을 붙인다. 섬(로비 모드)은 Set_Obj 가 무기를 안 붙이므로
|
||
// WLWeaponClassApplier 와 같이 호출 동안만 게임 모드를 Stage 로 바꾼다. 치수·카메라 값은 WLEquipGui.json `hub` (C45).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Equip
|
||
{
|
||
public class WLEquipPreview : UI3DModel
|
||
{
|
||
public static WLEquipPreview Instance { get; private set; }
|
||
public static string LastLog = "";
|
||
public static int Loads, ClassApplies;
|
||
public RenderTexture Texture { get { return _rt; } }
|
||
public int ShownClassId { get; private set; }
|
||
public string ShownPrefab { get; private set; }
|
||
public bool HasModel { get { return tf_model != null && tf_model.childCount > 1; } }
|
||
public bool Loading { get { return _loading; } }
|
||
|
||
Camera _cam; RenderTexture _rt; Light _light; WLGuiHub _h; PCActor _pc;
|
||
bool _loading; int _pendingClass; float _orientUntil = -1f;
|
||
|
||
public static WLEquipPreview Ensure(WLGuiHub h)
|
||
{
|
||
if (Instance != null) return Instance;
|
||
var go = new GameObject("WL_EquipPreviewRig");
|
||
DontDestroyOnLoad(go);
|
||
go.transform.position = new Vector3(0f, h.rigY, 0f);
|
||
var p = go.AddComponent<WLEquipPreview>();
|
||
p._h = h;
|
||
p.Build();
|
||
Instance = p;
|
||
return p;
|
||
}
|
||
|
||
void Build()
|
||
{
|
||
var h = _h;
|
||
_rt = new RenderTexture(Mathf.Max(64, h.previewRtW), Mathf.Max(64, h.previewRtH), 24, RenderTextureFormat.ARGB32);
|
||
_rt.name = "WL_EquipPreviewRT"; _rt.Create();
|
||
|
||
var camGo = new GameObject("Camera"); camGo.transform.SetParent(transform, false);
|
||
camGo.transform.localPosition = new Vector3(0f, h.camY, -h.camDist);
|
||
_cam = camGo.AddComponent<Camera>();
|
||
_cam.orthographic = true; _cam.orthographicSize = h.camOrtho;
|
||
_cam.nearClipPlane = 0.05f; _cam.farClipPlane = h.camDist * 2f + 5f;
|
||
_cam.clearFlags = CameraClearFlags.SolidColor; _cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
|
||
_cam.cullingMask = 1 << 5;
|
||
_cam.targetTexture = _rt; _cam.depth = -50f; _cam.allowHDR = false; _cam.allowMSAA = false; _cam.useOcclusionCulling = false;
|
||
_cam.enabled = false;
|
||
|
||
var model = new GameObject("tf_model"); model.transform.SetParent(transform, false);
|
||
tf_model = model.transform;
|
||
var lightGo = new GameObject("Light"); lightGo.transform.SetParent(tf_model, false); // 0 번 자식 = 빛(UI3DModel 규약 · Set_3DModel 이 1 번부터 지운다)
|
||
lightGo.transform.localRotation = Quaternion.Euler(h.lightPitch, h.lightYaw, 0f);
|
||
_light = lightGo.AddComponent<Light>();
|
||
_light.type = LightType.Directional; _light.cullingMask = 1 << 5; _light.shadows = LightShadows.None;
|
||
_light.intensity = h.lightIntensity; _light.enabled = h.lightIntensity > 0f; // 기본 0 = 장면 태양광(무한 방향광)만 — URP 메인 라이트 선정을 건드리지 않는다
|
||
}
|
||
|
||
public void SetVisible(bool on) { if (_cam != null) _cam.enabled = on; }
|
||
|
||
/// <summary>현재 코스튬·장착 무기(직업)로 모델을 맞춘다. 바뀐 것이 없으면 아무 일도 없다.</summary>
|
||
public void Refresh()
|
||
{
|
||
string prefab = null; int classId = 0;
|
||
try
|
||
{
|
||
var sd = ServerInfo.Ins != null ? ServerInfo.Ins.m_ServerData : null;
|
||
if (sd == null) { LastLog = "서버 데이터 없음(타이틀)"; return; }
|
||
var cd = table_costumelist.Ins.Get_Data_orNull(sd.Get_EqiupmentEquip(eItem.PCCostume));
|
||
// 실제 PC 와 같은 교체 규칙(PCInfo.cs:16 · PD #814 「붉은 기사 M05」) — 빠뜨리면 원본 코스튬 Ai01 이 나온다(2026-10-12 PD 지적)
|
||
prefab = cd != null ? WL.Character.WLCharacterSwapSettings.Resolve(cd.s_Prefab) : null;
|
||
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
||
if (w != null && w.itemJson != null) classId = WL.Combat.Weapon.WLWeaponAttackTable.ClassIdFor(w.itemJson.GroupNo);
|
||
if (classId <= 0 && MyValue.bMyPC && MyValue.MyPC != null) classId = MyValue.MyPC.Get_ID();
|
||
if (classId <= 0) classId = sd.Get_EqiupmentEquip(eItem.PC);
|
||
}
|
||
catch (System.Exception ex) { LastLog = "미리보기 정보 실패 — " + ex.Message; return; }
|
||
if (string.IsNullOrEmpty(prefab)) { LastLog = "코스튬 프리팹 없음"; return; }
|
||
|
||
if (_loading) { _pendingClass = classId; return; }
|
||
if (prefab == ShownPrefab && HasModel && _pc != null)
|
||
{
|
||
if (classId != ShownClassId) ApplyClass(_pc, classId);
|
||
return;
|
||
}
|
||
ShownPrefab = prefab; _pendingClass = classId; _pc = null; _loading = true; Loads++;
|
||
try
|
||
{
|
||
Set_3DModel(true, prefab, tr =>
|
||
{
|
||
_loading = false;
|
||
tr.localPosition = Vector3.zero; tr.localRotation = Quaternion.Euler(0f, _h.modelYaw, 0f);
|
||
_pc = tr.GetComponent<PCActor>();
|
||
if (_pc != null) ApplyClass(_pc, _pendingClass);
|
||
LastLog = "모델 " + prefab + " · 직업 " + ShownClassId;
|
||
});
|
||
}
|
||
catch (System.Exception ex) { _loading = false; LastLog = "모델 로드 실패 — " + ex.Message; }
|
||
}
|
||
|
||
static FieldInfo FindField(System.Type t, string name)
|
||
{
|
||
while (t != null)
|
||
{
|
||
var f = t.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||
if (f != null) return f;
|
||
t = t.BaseType;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>직업(무기 모델·애니메이터)을 UI 모델에 적용 — Change_Class 는 m_Stat 이 없는 UI 클론에서 예외를 내므로 필드 주입 + Set_Obj.</summary>
|
||
void ApplyClass(PCActor pc, int classId)
|
||
{
|
||
ShownClassId = classId;
|
||
ClassConfigTableData cd = null;
|
||
try { cd = table_classconfig.Ins.Get_Data_orNull(classId); } catch (System.Exception) { cd = null; }
|
||
if (cd == null) { LastLog = "ClassConfig 없음 " + classId; return; }
|
||
try { WL.Combat.Weapon.WLWeaponClasses.Ensure(); } catch (System.Exception) { }
|
||
try
|
||
{
|
||
var fClass = FindField(pc.GetType(), "m_ClassData"); if (fClass != null) fClass.SetValue(pc, cd);
|
||
var fId = FindField(pc.GetType(), "m_ID"); if (fId != null) fId.SetValue(pc, classId);
|
||
var fSd = FindField(pc.GetType(), "m_ServerData");
|
||
if (fSd != null && fSd.GetValue(pc) == null && ServerInfo.Ins != null) fSd.SetValue(pc, ServerInfo.Ins.m_ServerData);
|
||
}
|
||
catch (System.Exception ex) { LastLog = "필드 주입 실패 — " + ex.Message; }
|
||
|
||
var info = InGameInfo.Ins;
|
||
var gf = info != null ? typeof(InGameInfo).GetField("m_GameMode", BindingFlags.Instance | BindingFlags.NonPublic) : null;
|
||
object prev = gf != null ? gf.GetValue(info) : null; bool flipped = false;
|
||
try
|
||
{
|
||
if (gf != null && prev is eGameMode && (eGameMode)prev == eGameMode.Lobby) { gf.SetValue(info, eGameMode.Stage); flipped = true; }
|
||
pc.Set_Obj();
|
||
ClassApplies++;
|
||
}
|
||
catch (System.Exception ex) { LastLog = "Set_Obj 실패 — " + ex.Message; }
|
||
finally { if (flipped) gf.SetValue(info, prev); }
|
||
try { DSUtil.ChangeLayersRecursively(pc.transform, "UI"); } catch (System.Exception) { }
|
||
_orientUntil = Time.unscaledTime + 3f; // 무기 인스턴스는 Addressables 로 늦게 붙는다 → 3 s 동안 레이어·방향 보정을 다시 건다
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (_pc == null || _orientUntil < 0f || Time.unscaledTime > _orientUntil) return;
|
||
try
|
||
{
|
||
WL.Combat.Weapon.WLWeaponClasses.EnsureWeaponOrient(_pc);
|
||
DSUtil.ChangeLayersRecursively(_pc.transform, "UI");
|
||
}
|
||
catch (System.Exception) { }
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
if (Instance == this) Instance = null;
|
||
if (_rt != null) { _rt.Release(); Destroy(_rt); _rt = null; }
|
||
}
|
||
}
|
||
}
|