187 lines
14 KiB
C#
187 lines
14 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// WLWeaponClassApplier.cs — 장착 무기 → 힘민지 직업(모션 세트) 전환 (#825 · 2026-10-09 · 기획 v1 판단 ① 권고 A)
|
|
//
|
|
// 장착한 무기(FR GroupNo)의 표 class_id 로 `PCActor.Change_Class(cd, true)` 를 부른다 → 애니메이터·무기 모델·사거리·탄(Projectile_{CID})이
|
|
// 함께 바뀐다. `_ui = true` 라 서버 세이브(Equip[PC])는 건드리지 않는다(힘민지 ServerInfo.Write 오염 방지 · 코드 조사 2026-10-09).
|
|
// - 켜고 끄기 = WLWeaponSettings.classSwitch (PD 판단 ① 확정 전 기본 0)
|
|
// - 섬(로비 맵)에서는 Set_Obj 가 로비 애니메이터를 끼우므로 WLIslandAvatar.ApplyCombatForm 과 같은 방식으로 호출 동안만 Stage 로 바꾼다
|
|
// - 공격·스킬 모션 중에는 바꾸지 않고, 전환 뒤 settleSec 동안은 다시 바꾸지 않는다(컨트롤러 비동기 로드)
|
|
// - 전환 뒤 WLEquipStats.RefreshPc 로 장비 스탯을 다시 반영한다
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using UnityEngine;
|
|
using WL.Equip;
|
|
|
|
namespace WL.Combat.Weapon
|
|
{
|
|
// WLWeaponSettings 는 WLWeaponSettings.cs 로 옮김(#883 · 파일 이름 = 클래스 이름이어야 에셋이 스크립트를 참조한다)
|
|
|
|
public class WLWeaponClassApplier : MonoBehaviour
|
|
{
|
|
public static WLWeaponClassApplier Instance { get; private set; }
|
|
public static int Applied, Skipped, Forced;
|
|
/// <summary>#952-④ 마지막 직업 전환 시각(unscaled) — 전환 직후 컨트롤러가 비동기로 바뀌는 동안(settleSec) 첫 시전(first_cast)이 나가면 스킬 동작이 옛 컨트롤러에서 돌아 타격 이벤트가 없고(WLJumpSlam 「타격 이벤트 없음」 실측) 스킬이 끈 PCMove 가 안 돌아온다 → WLWeaponSkillSlots.TryFirstCast 가 이 시각으로 미룬다.</summary>
|
|
public static float LastSwitchAt = -999f;
|
|
public static bool Settling { get { var cfg = WLWeaponSettings.Instance; return Time.unscaledTime - LastSwitchAt < (cfg != null ? cfg.settleSec : 1.5f); } }
|
|
public static string LastLog { get; private set; } = "";
|
|
public static int LastClassId, DesiredClassId;
|
|
|
|
float _t; float _settleUntil; float _pendingSince = -1f;
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
var cfg = WLWeaponSettings.Instance;
|
|
if (cfg == null || cfg.enabled_ == 0 || Instance != null) return;
|
|
var go = new GameObject("WL_WeaponClassApplier");
|
|
DontDestroyOnLoad(go);
|
|
Instance = go.AddComponent<WLWeaponClassApplier>();
|
|
}
|
|
|
|
void OnEnable() { WLInventory.Changed += OnInventoryChanged; }
|
|
void OnDisable() { WLInventory.Changed -= OnInventoryChanged; }
|
|
void OnDestroy() { if (Instance == this) Instance = null; }
|
|
void OnInventoryChanged() { _t = 999f; } // 다음 Update 에서 바로 점검
|
|
|
|
void Update()
|
|
{
|
|
var cfg = WLWeaponSettings.Instance;
|
|
if (cfg == null || cfg.enabled_ == 0 || cfg.classSwitch == 0) return;
|
|
_t += Time.unscaledDeltaTime;
|
|
if (_t < cfg.pollSec) return;
|
|
_t = 0f;
|
|
TryApply(cfg, "poll");
|
|
}
|
|
|
|
/// <summary>
|
|
/// #851 PD 「던전에 들어갈 때 멋대로 무기가 바뀌면 안돼」 — 씬마다 새로 만들어지는 내 PC 를 **처음부터** 장착 무기의 직업으로 만든다.
|
|
/// 원본은 서버 데이터의 장착 직업(Equip[PC] · 10501)으로 PC 를 만들고, 이 적용기가 폴링으로 뒤늦게 바꿔 「해머 → 검」 교체가 보였다.
|
|
/// 원본 훅 = PCActor.Set 1줄. 무기가 없거나 표에 없으면 받은 값 그대로(원본 동작).
|
|
/// </summary>
|
|
public static int ResolveSpawnClass(int baseClassId)
|
|
{
|
|
var cfg = WLWeaponSettings.Instance;
|
|
if (cfg == null || cfg.enabled_ == 0 || cfg.classSwitch == 0) return baseClassId;
|
|
try
|
|
{
|
|
if (!WLInventory.EnsureLoaded()) return baseClassId;
|
|
WLStarterKit.Ensure(cfg); // 🔴 임시 시작 장비(검 장착)를 출생 전에 — 시작 직후 「해머 → 검」 교체도 없앤다(세이브 객체당 1회 · 멱등)
|
|
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
|
int desired = w != null && w.itemJson != null ? WLWeaponAttackTable.ClassIdFor(w.itemJson.GroupNo) : 0;
|
|
if (desired <= 0 || desired == baseClassId) return baseClassId;
|
|
WLWeaponClasses.Ensure(); // WL 추가 직업(창·대검) 행을 ClassConfig 에 보탠다(멱등)
|
|
var cd = table_classconfig.Ins != null ? table_classconfig.Ins.Get_Data_orNull(desired) : null;
|
|
if (cd == null) return baseClassId;
|
|
SpawnResolved++;
|
|
LastSpawnLog = "spawn: 직업 " + baseClassId + " → " + desired + " (" + w.itemJson.Item_name + ")";
|
|
if (cfg.verboseLog != 0) Debug.Log("[WL-825] " + LastSpawnLog);
|
|
return desired;
|
|
}
|
|
catch (System.Exception) { return baseClassId; } // 원본은 없는 ID 에 예외를 던진다 — 원래 값으로
|
|
}
|
|
public static int SpawnResolved;
|
|
public static string LastSpawnLog = "";
|
|
|
|
static int CurrentClassId(PCActor pc)
|
|
{
|
|
if (pc == null) return 0;
|
|
var f = typeof(Actor).GetField("m_ID", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
|
object v = f != null ? f.GetValue(pc) : null;
|
|
return v is int ? (int)v : 0;
|
|
}
|
|
|
|
/// <summary>장착 무기의 직업으로 전환을 시도한다. 돌려주는 값 = 결과 한 줄(프로브·로그용).</summary>
|
|
public string TryApply(WLWeaponSettings cfg, string why)
|
|
{
|
|
if (!MyValue.bMyPC || MyValue.MyPC == null) return Note("PC 없음");
|
|
var pc = MyValue.MyPC;
|
|
if (!WLInventory.EnsureLoaded()) return Note("인벤토리 미로드");
|
|
WLStarterKit.Ensure(cfg); // 🔴 임시(PD 2026-10-10) — 무기 6계열 1개씩 + 검 장착 · 1회
|
|
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
|
int desired = w != null && w.itemJson != null ? WLWeaponAttackTable.ClassIdFor(w.itemJson.GroupNo) : 0;
|
|
DesiredClassId = desired;
|
|
int cur = CurrentClassId(pc);
|
|
LastClassId = cur;
|
|
if (desired <= 0) { Skipped++; return Note(w == null ? "무기 미장착" : "표에 없는 무기 그룹 " + w.itemJson.GroupNo); }
|
|
WLWeaponClasses.Ensure(); // WL 추가 직업(창) 행을 ClassConfig 에 보탠다(멱등)
|
|
WLWeaponClasses.EnsureWeaponOrient(pc); // #834 활 등 무기 인스턴스 방향 보정(표식 1회 · 재장착 때 재적용)
|
|
if (cur == desired)
|
|
{
|
|
_pendingSince = -1f;
|
|
bool ev = WLWeaponClasses.EnsureClipEvents(pc, cur); // 추가 직업 = 클립 타격 이벤트 보강(컨트롤러 로드 뒤)
|
|
bool rm = pc.Get_CurAnim() < eAnim.Attack && WLWeaponClasses.RefreshModelIfNeeded(pc, cur); // 수정 행(망치) = 살아 있는 PC 의 무기 모델 재부착 1회
|
|
return Note("이미 직업 " + desired + " (" + w.itemJson.Item_name + ")" + (ev ? "" : " · 클립 이벤트 대기") + (rm ? " · 모델 재부착" : ""));
|
|
}
|
|
if (Time.unscaledTime < _settleUntil) { Skipped++; return Note("안정화 대기"); }
|
|
bool forced = false;
|
|
if (pc.Get_CurAnim() >= eAnim.Attack)
|
|
{
|
|
// 연속 전투에서는 공격 끝 프레임을 폴링으로 못 잡는다(2026-10-09 스모크: 6종 전부 「보류」) → 일정 시간 뒤 공격을 끊고 바꾼다
|
|
if (_pendingSince < 0f) _pendingSince = Time.unscaledTime;
|
|
if (cfg.forceSwitchAfterSec <= 0f || Time.unscaledTime - _pendingSince < cfg.forceSwitchAfterSec) { Skipped++; return Note("공격 중 — 보류"); }
|
|
try { pc.Play_Idle(); } catch (System.Exception) { }
|
|
Forced++; forced = true;
|
|
}
|
|
bool stalePcMove = false; try { stalePcMove = OptionInfo.Ins != null && !OptionInfo.Ins.Get_PCMove(); } catch (System.Exception) { } // #952-④ 스킬이 꺼 둔 가상패드 이동 허용이 대기 상태에 남아 있음 = 끊긴 스킬의 잔재
|
|
_pendingSince = -1f;
|
|
ClassConfigTableData cd = null;
|
|
try { cd = table_classconfig.Ins != null ? table_classconfig.Ins.Get_Data_orNull(desired) : null; } catch (System.Exception) { cd = null; } // 원본은 없는 ID 에 예외를 던진다
|
|
if (cd == null) { Skipped++; return Note("ClassConfig 없음 " + desired); }
|
|
|
|
// 섬(로비 맵)은 Set_Obj 가 로비 애니메이터를 끼운다 → 호출 동안만 Stage 로(WLIslandAvatar.ApplyCombatForm 과 같은 방식)
|
|
var info = InGameInfo.Ins;
|
|
var gf = info != null ? typeof(InGameInfo).GetField("m_GameMode", System.Reflection.BindingFlags.Instance | System.Reflection.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.Change_Class(cd, true);
|
|
}
|
|
catch (System.Exception ex) { return Note("전환 실패 — " + ex.Message); }
|
|
finally { if (flipped) gf.SetValue(info, prev); }
|
|
try { WLEquipStats.RefreshPc(); } catch (System.Exception ex) { Debug.LogWarning("[WL-825] 스탯 재반영 실패 — " + ex.Message); }
|
|
Applied++; LastSwitchAt = Time.unscaledTime;
|
|
if (forced || stalePcMove) RestoreMotion(pc, forced ? "강제 전환" : "PCMove 꺼진 채 전환"); // #952-④
|
|
_settleUntil = Time.unscaledTime + cfg.settleSec;
|
|
return Note(why + ": 직업 " + cur + " → " + desired + " (" + w.itemJson.Item_name + " · " + WLWeaponAttackTable.Describe(w.itemJson.GroupNo) + ")" + (flipped ? " · 로비 모드 우회" : "") + (forced || stalePcMove ? " · 상태 원상(" + LastRestore + ")" : ""));
|
|
}
|
|
|
|
public static int Restored; public static string LastRestore = "";
|
|
static System.Reflection.PropertyInfo s_pIsStop; static System.Reflection.FieldInfo s_fImmortal; static System.Reflection.MethodInfo s_mModel, s_mWeapon;
|
|
/// <summary>#952-④ PD 「마법책 무기 변경 후 캐릭터 이동이 멈추는 현상」 — 스킬(해머 강력한 일격 등) 도중 직업을 강제로 바꾸면(Play_Idle → Change_Class) 그 스킬이 끝에 되돌릴 상태가 남는다:
|
|
/// 가상패드(FloatingJoystick)가 읽는 OptionInfo.PCMove(false 면 드래그·뗌을 모두 무시 → 뗌을 못 받아 IsStop·JoystickStatus 1 이 굳는다 = PD 에디터 실측) · 무적 · 모델 숨김 · 애니메이터 속도 0 · WL 시전 잠금 · 착지 연출. 전환 직후 원상.</summary>
|
|
static void RestoreMotion(PCActor pc, string why)
|
|
{
|
|
var sb = new System.Text.StringBuilder(why);
|
|
try { if (OptionInfo.Ins != null && !OptionInfo.Ins.Get_PCMove()) { OptionInfo.Ins.Set_PCMove(pc, true); sb.Append(" · PCMove"); } } catch (System.Exception) { }
|
|
try { if (pc.m_animation != null && pc.m_animation.speed != 1f) { pc.m_animation.speed = 1f; sb.Append(" · 속도 1"); } } catch (System.Exception) { }
|
|
try
|
|
{
|
|
var t = typeof(Actor); var bf = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public;
|
|
if (s_fImmortal == null) s_fImmortal = t.GetField("m_bImmortal", bf);
|
|
if (s_fImmortal != null && (bool)s_fImmortal.GetValue(pc)) { s_fImmortal.SetValue(pc, false); sb.Append(" · 무적 해제"); }
|
|
if (s_mModel == null) s_mModel = t.GetMethod("Model_OnOff", bf);
|
|
if (s_mWeapon == null) s_mWeapon = t.GetMethod("Weapon_OnOff", bf);
|
|
bool hidden = false; foreach (var r in pc.GetComponentsInChildren<SkinnedMeshRenderer>(true)) if (r != null && !r.enabled) { hidden = true; break; }
|
|
if (hidden) { if (s_mModel != null) s_mModel.Invoke(pc, new object[] { true }); if (s_mWeapon != null) s_mWeapon.Invoke(pc, new object[] { true }); sb.Append(" · 모델 보임"); }
|
|
if (s_pIsStop == null) s_pIsStop = t.GetProperty("IsStop", bf);
|
|
if (s_pIsStop != null && pc.JoystickStatus == 0 && (bool)s_pIsStop.GetValue(pc, null)) { s_pIsStop.SetValue(pc, false, null); sb.Append(" · IsStop 해제"); } // 조이스틱을 쥔 상태(1)의 IsStop 은 뗄 때 원본이 푼다
|
|
}
|
|
catch (System.Exception ex) { sb.Append(" · 리플렉션 실패 ").Append(ex.Message); }
|
|
try { if (pc.JoystickStatus == 2) { pc.Set_JoystickStatus(3); sb.Append(" · 조이스틱 2→0"); } } catch (System.Exception) { }
|
|
try { WLSkillCancel.ResetFor(pc); } catch (System.Exception) { }
|
|
try { WLGhostStrike.CancelFor(pc); } catch (System.Exception) { }
|
|
Restored++; LastRestore = sb.ToString();
|
|
}
|
|
|
|
string Note(string s)
|
|
{
|
|
LastLog = s;
|
|
var cfg = WLWeaponSettings.Instance;
|
|
if (cfg != null && cfg.verboseLog != 0 && s.Contains("→")) Debug.Log("[WL-825] " + s);
|
|
return s;
|
|
}
|
|
}
|
|
}
|