Project_WL/Assets/WL/IslandMob/WLTrailTest.cs

141 lines
7.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLTrailTest.cs — 궤적(검기) 테스트 모드: 섬 몬스터 1마리만 등장 + 무적
//
// PD 지시 (2026-10-10): 「우선 궤적 테스트를 위해 몬스터를 1마리만 등장시키고 무적 상태로 만들어줘.」
//
// ■ 켜기/끄기 = PlayerPrefs `WL_TrailTest` (1/0) — 에디터 프로브 AgentScripts/staging/WL823/p23_trailtest_{on,off,state}.cs.
// 빌드·세이브·표·프리팹 무변경. 끄면 원본 존 동작 그대로(C8 롤백 경로).
// ■ 2026-10-15 PD 「섬에 타격감을 체크할 수 있도록 무적 몬스터 1기를 다시 추가」 — 상시 스위치 = `WLIslandMobSettings.hitTestDummy`
// (몬스터 존이 꺼져 있으면 WLIslandMobZones 가 이 1마리 전용 자리만 만든다) · 종류 = `hitTestDummyMobId`(다른 종류는 회수하고 기다린다).
// ■ 동작 — WLIslandMobZones.OnSpawned 의 두 줄 훅:
// ① 더미가 살아 있는데 몹이 또 등장하면 즉시 회수(Actor.Off · 존 누적에 안 센다). 리젠은 원본대로 돌되 화면엔 1마리.
// ② 첫 몹을 더미로 채택 — MaxHP·HP 를 큰 값으로 덮고(ApplyMobHp 뒤) 매 LateUpdate 되채운다.
// 🔴 왜 m_bImmortal(Is_Immortal) 을 쓰지 않는가 — ActorInfo.Get_EnemyActors(:328)·Get_AlliesActors(:365) 가
// Is_Immortal 액터를 대상 목록에서 **제외**한다 → PC 자동 전투가 그 몹을 아예 때리지 않는다.
// 궤적을 보려면 맞아야 하므로 「체력 되채움」으로 무적을 만든다.
// 🔴 Actor.Get_Damage 는 HP 를 깎고 0 이하면 그 자리에서 죽이므로 LateUpdate 되채움만으론 한 방 사망을 못 막는다
// → MaxHP 자체를 큰 값으로 올린다(플레이어 한 방 피해보다 충분히 큼).
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.IslandMob
{
public static class WLTrailTest
{
public const string PrefsKey = "WL_TrailTest";
/// <summary>더미 체력. 게임 표 값이 아니라 테스트 장치 값 — 플레이어 한 방 피해보다 충분히 크면 된다.</summary>
public static double DummyHp = 1e9;
/// <summary>검증 전용 — 0/1 이면 PlayerPrefs 대신 이 값을 쓴다(-1 = PlayerPrefs).</summary>
public static int Override = -1;
public static Actor Dummy;
public static int Adopted, Recalled, Refills;
public static string LastLog = "";
/// <summary>켜짐 = PlayerPrefs WL_TrailTest(에디터 프로브) 또는 설정 `WLIslandMobSettings.hitTestDummy`(2026-10-15 PD 「타격감 체크용 무적 몬스터 1기」 · 상시 스위치).</summary>
public static bool Enabled
{
get
{
if (Override >= 0) return Override != 0;
if (PlayerPrefs.GetInt(PrefsKey, 0) != 0) return true;
var c = WLIslandMobSettings.Instance;
return c != null && c.hitTestDummy != 0;
}
}
public static bool DummyAlive { get { return Dummy != null && Dummy.isActiveAndEnabled && !Dummy.IsDead(); } }
/// <summary>더미로 쓸 종류인가 — 설정 `hitTestDummyMobId`(0 = 아무 몹).</summary>
public static bool Wanted(Actor a)
{
var c = WLIslandMobSettings.Instance;
int want = c != null ? c.hitTestDummyMobId : 0;
return a != null && (want <= 0 || MonsterId(a) == want);
}
static System.Reflection.FieldInfo s_mobData;
/// <summary>
/// 몹의 MonsterList ID(n_MonsterID). 🔴 실측(2026-10-15): `Actor.Get_ID()` 는 섬 스포너 몹 전부 1 을 돌려준다(몬스터 ID 아님) →
/// 원본 `MobActor.m_MonsterTableData`(protected · 공개 접근자 없음)를 읽는다. 못 읽으면 0.
/// </summary>
public static int MonsterId(Actor a)
{
var mob = a as MobActor;
if (mob == null) return 0;
if (s_mobData == null)
s_mobData = typeof(MobActor).GetField("m_MonsterTableData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var d = s_mobData != null ? s_mobData.GetValue(mob) as MonsterTableData : null;
return d != null ? d.n_MonsterID : 0;
}
/// <summary>
/// 등장한 몹을 회수해야 하는가 — ① 더미가 이미 살아 있고 이 몹이 그 더미가 아닐 때 ② 더미가 없는데 원하는 종류가 아닐 때(다음 리젠을 기다린다).
/// 반환 2 = ①(이 존의 리젠 예약도 멈춘다) · 1 = ②(리젠은 계속) · 0 = 회수 안 함.
/// </summary>
public static int RecallKind(Actor a)
{
if (!Enabled || a == null) return 0;
if (DummyAlive) return ReferenceEquals(Dummy, a) ? 0 : 2;
return Wanted(a) ? 0 : 1;
}
public static bool ShouldRecall(Actor a) { return RecallKind(a) != 0; }
/// <summary>첫 몹을 더미로 채택한다. ApplyMobHp 뒤에 불러야 HP 덮기가 남는다.</summary>
public static void Adopt(Actor a)
{
if (!Enabled || a == null) return;
if (DummyAlive && !ReferenceEquals(Dummy, a)) return;
if (!Wanted(a)) return;
Dummy = a;
var d = a.GetComponent<WLTrailTestDummy>();
if (d == null) d = a.gameObject.AddComponent<WLTrailTestDummy>();
d.Begin(a);
Adopted++;
LastLog = a.name + "(몬스터 " + MonsterId(a) + ") 더미 채택 · HP " + a.Get_HP().ToString("F0") + "/" + a.Get_MaxHP().ToString("F0") + " · " + a.Get_position().ToString("F1");
}
/// <summary>보고·프로브용 한 줄.</summary>
public static string Describe()
{
return "enabled=" + Enabled + " dummy=" + (Dummy != null ? Dummy.name : "-") + " alive=" + DummyAlive +
" hp=" + (DummyAlive ? Dummy.Get_HP().ToString("F0") + "/" + Dummy.Get_MaxHP().ToString("F0") : "-") +
" adopted=" + Adopted + " recalled=" + Recalled + " refills=" + Refills + " · " + LastLog;
}
}
/// <summary>더미 몹에 붙어 체력을 되채운다. 모드를 끄면 아무것도 하지 않는다(그때부터 평범하게 죽는다).</summary>
public sealed class WLTrailTestDummy : MonoBehaviour
{
Actor _a;
double _hp;
public void Begin(Actor a)
{
_a = a;
_hp = WLTrailTest.DummyHp;
Fill();
}
void Fill()
{
if (_a == null) return;
var st = _a.Get_StatInfo();
if (st == null) return;
if (_a.Get_MaxHP() < _hp) st.Set_Stat(eStat.MaxHP, _hp);
if (_a.Get_HP() < _hp) { st.Set_Stat(eStat.HP, _hp); WLTrailTest.Refills++; }
}
void LateUpdate()
{
if (_a == null || !WLTrailTest.Enabled || _a.IsDead()) return;
Fill();
}
}
}