Project_WL/Assets/WL/Equip/WLEquipPreview.cs

172 lines
9.8 KiB
C#
Raw Permalink 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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; }
}
}
}