Project_WL/Assets/WL/SkillTest/WLSkillTestHarness.cs

305 lines
19 KiB
C#
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillTestHarness.cs — 독립 스킬 테스트 씬(WLSkillTest) · 캐릭터 + 스킬 시스템만 따로 세운 하네스 (PD #947 재지시 · 2026-10-20)
//
// PD 원문: 「이 프로젝트를 기반으로 만들라는 의미가 아니라, 스킬 애니메이션 및 연출을 위해 캐릭터 및 스킬 시스템만 별도로 구축해서
// 테스트 목적으로 쓸 씬을 구축해달라는 뜻」
//
// ■ 방식 = 원본 개발자의 StatTest 하네스(MainToStatTest)와 같은 줄기: **로그인 0 · HUD(NewGameUI) 0 · 섬·로비 0 · Addressables 부트 0**.
// 씬에 직접 둔 것만으로 돈다 — 테이블 프리팹 · ActorInfo · InGameInfo(이펙트·투사체 풀) · ProjectileInfo · 원본 Main Camera · 치비 PC 프리팹 · 더미 몹 프리팹 · 평지.
// 순서: 평지 NavMesh 베이크 → 테이블 로딩 대기 → 오프라인 ServerData(신규 유저 꼴 · 자동 전투 1) → PC.Set(직업 = 낀 WL 무기) → 더미 Set(무적 · 제자리) → 카메라 리그 → ready.
// ■ 원본 코드와의 접점 = StatTest 전용 분기 13곳을 `WLTestScene.Active`(StatTest 또는 WLSkillTest)로 넓힌 것뿐(HUD·포털·서버 일시정지·대상 목록 분기 · 전부 #if UNITY_EDITOR 안).
// 이동·넉백·소환 AI 분기(Actor 1665·1710·2198 · MobActor 75 · SummonActor 60 · WizardActor 53·61)는 StatTest 전용으로 남겨 **실제 게임 동작**을 유지한다.
// ■ 이 클래스가 MainToStatTest 를 상속하는 이유 = 원본 분기가 `MainToStatTest.Ins.actors[1]`(대상)·`Set_Dmglabel`(피해 글자)를 부른다 → 같은 자리를 채워 준다.
// ■ 키: WASD/방향키 이동 · Space 기본 공격 1타 · Q 주력 · E 각성 · 숫자 등급(기존 훅) · [ ] 무기 바꾸기 · R 더미 제자리 · T 슬로 모션 · P 일시정지
// ■ 값 = Resources/WL/WLSkillTest.json (C45) · 상태 파일 = Library/WLSkillTest_status.txt(외부 스크립트는 Play 중 eval 대신 이 파일) · 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업)
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.SceneManagement;
using WL.Combat.Weapon;
using WL.Equip;
using WL.Island;
using WL.IslandMob;
namespace WL.SkillTest
{
/// <summary>원본 코드의 StatTest 전용 분기가 이 씬에서도 서도록 넓힌 판정(에디터 전용 분기 안에서만 쓰인다).</summary>
public static class WLTestScene
{
public const string Name = "WLSkillTest";
public static bool Active { get { var n = SceneManager.GetActiveScene().name; return n == Name || n == "StatTest"; } }
}
[System.Serializable]
public class WLSkillTestJson
{
public string note = "";
public int baseClassId = 10101; // 낀 WL 무기가 없을 때 PC 직업(묵광 검+방패)
public int autoBattle = 1; // 1 = 섬처럼 자동 전투(대상 있으면 기본 공격 콤보)
public float[] spawnPos = { 0f, 0f, 0f };
public int cameraRig = 1; // 1 = 섬 구도 카메라 리그(WLDungeonCameraRig)
public int dummyMobId = 0; // 0 = WLIslandMobSettings.hitTestDummyMobId (몬스터 표 행 · 능력치)
public float[] dummyOffset = { 0f, 0f, 0f }; // 전부 0 = WLIslandMobSettings.hitTestDummyOffset
public int dummyStill = 1; // 1 = 더미 제자리
public int moveKeys = 1;
public string attackKey = "Space";
public string weaponPrevKey = "LeftBracket";
public string weaponNextKey = "RightBracket";
public string dummyResetKey = "R";
public string slowKey = "T";
public float slowScale = 0.2f;
public string pauseKey = "P";
public int label = 1;
public float labelFontScale = 0.018f;
public float groundSize = 100f;
public float[] groundColor = { 0.45f, 0.62f, 0.35f };
public float[] skyColor = { 0.55f, 0.75f, 0.95f };
}
public sealed class WLSkillTestHarness : MainToStatTest
{
public const string ResourcesPath = "WL/WLSkillTest";
[Header("씬 빌더가 채운다")]
public Actor pcActor; // 치비 PC 프리팹 인스턴스(씬에 직접)
public MobActor dummyActor; // 더미 몹 프리팹 인스턴스(씬에 직접)
public static string Stage = "none", LastLog = "";
public static bool Ready;
public static Actor Dummy;
public static readonly List<string> Laps = new List<string>();
public static readonly List<string> Hangs = new List<string>();
public static float T0;
static WLSkillTestJson s_cfg;
float _lastRt;
bool _moving, _slow;
KeyCode _kAttack = KeyCode.Space, _kPrev = KeyCode.LeftBracket, _kNext = KeyCode.RightBracket, _kReset = KeyCode.R, _kSlow = KeyCode.T, _kPause = KeyCode.P;
string _msg = "";
GUIStyle _style;
public static WLSkillTestJson Cfg
{
get
{
if (s_cfg != null) return s_cfg;
s_cfg = new WLSkillTestJson();
var ta = Resources.Load<TextAsset>(ResourcesPath);
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
return s_cfg;
}
}
public static string StatusPath { get { return System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, "../Library/WLSkillTest_status.txt")); } }
static Vector3 V3(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; }
static KeyCode Key(string s, KeyCode d) { KeyCode k; return !string.IsNullOrEmpty(s) && System.Enum.TryParse(s, true, out k) ? k : d; }
static void WriteStatus() { try { System.IO.File.WriteAllText(StatusPath, Describe()); } catch { } }
static void Lap(string name) { Stage = name; Laps.Add(name + " " + (Time.realtimeSinceStartup - T0).ToString("F1") + "s"); WriteStatus(); }
// MainToStatTest.Awake 는 go_tables 를 복제한다 — 여기선 테이블이 씬에 직접 있으므로 싱글턴 자리만 잡는다
protected override void Awake()
{
Ins = this; isIns = true;
T0 = Time.realtimeSinceStartup;
Laps.Clear(); Hangs.Clear(); Ready = false; Dummy = null; s_cfg = null; LastLog = "";
var cfg = Cfg;
_kAttack = Key(cfg.attackKey, KeyCode.Space); _kPrev = Key(cfg.weaponPrevKey, KeyCode.LeftBracket); _kNext = Key(cfg.weaponNextKey, KeyCode.RightBracket);
_kReset = Key(cfg.dummyResetKey, KeyCode.R); _kSlow = Key(cfg.slowKey, KeyCode.T); _kPause = Key(cfg.pauseKey, KeyCode.P);
if (actors == null || actors.Length < 2) actors = new Actor[] { pcActor, dummyActor };
Lap("awake");
}
IEnumerator Start()
{
var cfg = Cfg;
BakeGround(); ApplySky(cfg);
Lap("navmesh");
while (TableChecker.Ins == null || !TableChecker.Ins.CheckAllLoad()) yield return null;
Lap("tables");
if (pcActor == null || dummyActor == null) { Stage = "refs-missing"; LastLog = "pcActor/dummyActor 참조 없음 — Build Scene 다시"; WriteStatus(); yield break; }
// 오프라인 서버 데이터(신규 유저 꼴 · 테이블 로딩 뒤에만 생성 가능) + 자동 전투
var sdata = new ServerData();
try { if (cfg.autoBattle != 0) sdata.Add_Common(eCommon.Auto, 1); } catch (System.Exception ex) { LastLog = "Auto 설정 실패 " + ex.Message; }
// PC — 직업 = 낀 WL 무기(없으면 baseClassId). 원본 Set 안에서 WL 무기 적용기·스타터 킷이 돈다(섬과 같은 경로)
int cls = ResolveClass(cfg);
pcActor.transform.position = NavPos(V3(cfg.spawnPos));
pcActor.Set(false, null, sdata, cls, true);
ActorInfo.Ins.Add_Actor(pcActor, eRole.PC, 0, pcActor.name);
Lap("pc " + cls);
// 더미 — 몬스터 표 행으로 능력치 · 무적(WLTrailTest 체력 되채움) · 제자리
var ms = WLIslandMobSettings.Instance;
int mobId = cfg.dummyMobId > 0 ? cfg.dummyMobId : (ms != null ? ms.hitTestDummyMobId : 101);
var data = table_monsterlist.Ins.Get_Data_orNull(mobId);
if (data == null) { Stage = "mob-row-none"; LastLog = "몬스터 표 없음 " + mobId; WriteStatus(); yield break; }
ActorInfo.Ins.Add_Actor(dummyActor, eRole.Mob, 1, dummyActor.name);
dummyActor.Set(true, null, null, 1, false);
dummyActor.Set(data, DummyPos(cfg, ms), null, 0);
if (cfg.dummyStill != 0) dummyActor.Stop(true);
WLTrailTest.Adopt(dummyActor); Dummy = dummyActor;
if (dummyActor.GetComponent<WLTrailTestDummy>() == null) dummyActor.gameObject.AddComponent<WLTrailTestDummy>().Begin(dummyActor);
Lap("dummy " + mobId);
// 카메라 — 섬 구도 리그 + 원본 RealCamera 대상
var icfg = WLIslandSettings.Instance;
if (icfg != null && cfg.cameraRig != 0 && Object.FindFirstObjectByType<WLDungeonCameraRig>() == null) WLDungeonCameraRig.Create(icfg, gameObject.scene);
var rc = MyValue.m_RealCamera;
if (rc != null) rc.Set_Target(pcActor.transform, icfg != null ? WLDungeonCameraRig.SolvedYaw(icfg) : 0f);
yield return null;
pcActor.Stop(false);
pcActor.Change_Target(false, dummyActor);
Ready = true;
Lap("ready");
}
void OnDestroy() { try { if (System.IO.File.Exists(StatusPath)) System.IO.File.Delete(StatusPath); } catch { } Time.timeScale = 1f; }
// ─────────────────────────────────────────────────────────────────
static int ResolveClass(WLSkillTestJson cfg)
{
try
{
if (WLInventory.EnsureLoaded())
{
var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
if (w != null && w.itemJson != null) { int c = WLWeaponAttackTable.ClassIdFor(w.itemJson.GroupNo); if (c > 0) return c; }
}
}
catch (System.Exception ex) { LastLog = "직업 판정 실패 " + ex.Message; }
return cfg.baseClassId > 0 ? cfg.baseClassId : 10101;
}
static Vector3 NavPos(Vector3 want)
{
UnityEngine.AI.NavMeshHit hit;
return UnityEngine.AI.NavMesh.SamplePosition(want, out hit, 5f, UnityEngine.AI.NavMesh.AllAreas) ? hit.position : want;
}
static Vector3 DummyPos(WLSkillTestJson cfg, WLIslandMobSettings ms)
{
Vector3 off = V3(cfg.dummyOffset); if (off == Vector3.zero && ms != null) off = ms.hitTestDummyOffset;
var pc = MyValue.MyPC;
Vector3 basePos = pc != null ? pc.Get_position() : V3(cfg.spawnPos);
Quaternion rot = pc != null ? Quaternion.Euler(0f, pc.transform.eulerAngles.y, 0f) : Quaternion.identity;
return NavPos(basePos + rot * off);
}
static void ApplySky(WLSkillTestJson cfg)
{
var cam = Camera.main;
if (cam == null || cfg.skyColor == null || cfg.skyColor.Length < 3) return;
if (RenderSettings.skybox == null) { cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(cfg.skyColor[0], cfg.skyColor[1], cfg.skyColor[2]); }
}
void BakeGround()
{
var surf = Object.FindFirstObjectByType<NavMeshSurface>();
if (surf == null) { var ground = GameObject.Find("Ground"); if (ground == null) return; surf = ground.AddComponent<NavMeshSurface>(); }
surf.useGeometry = UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders;
surf.BuildNavMesh();
}
/// <summary>더미를 PC 앞 기본 자리로(R 키 · 프로브).</summary>
public static string ResetDummy()
{
var d = Dummy; if (d == null || MyValue.MyPC == null) return "PC/더미 없음";
var pos = DummyPos(Cfg, WLIslandMobSettings.Instance);
d.Stop(true); d.Set_Warp(pos);
return "더미 " + pos.ToString("F1");
}
/// <summary>낀 무기를 인벤토리 무기 목록에서 앞뒤로(dir ±1).</summary>
public static string CycleWeapon(int dir)
{
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
var ids = new List<int>(); var names = new List<string>(); int curIdx = -1;
foreach (var it in WLInventory.SortedItems())
{
if (it == null || it.itemJson == null || !it.itemJson.IsEquipment || it.itemJson.EquipmentType != eFREquipmentType.Weapon) continue;
if (cur != null && ReferenceEquals(it, cur)) curIdx = ids.Count;
ids.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + it.itemJson.GroupNo + ")");
}
if (ids.Count == 0) return "무기 없음";
int next = curIdx < 0 ? 0 : (curIdx + dir + ids.Count) % ids.Count;
bool ok = WLInventory.Equip(ids[next]);
return (ok ? "장착 " : "실패 ") + names[next] + " " + (next + 1) + "/" + ids.Count + (ok ? "" : " · " + WLInventory.LastMessage);
}
public static string Describe()
{
var pc = MyValue.MyPC; string w = "-";
if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; }
return "stage=" + Stage + " ready=" + Ready + " pc=" + (pc != null ? pc.name + pc.Get_position().ToString("F1") : "-") + " weapon=" + w +
" dummy=" + (Dummy != null ? Dummy.name + Dummy.Get_position().ToString("F1") + " hp " + Dummy.Get_HP().ToString("F0") : "-") +
" laps=[" + string.Join(" · ", Laps.ToArray()) + "] hangs=[" + string.Join(" ", Hangs.ToArray()) + "] " + LastLog;
}
// ─────────────────────────────────────────────────────────────────
void LateUpdate()
{
float now = Time.realtimeSinceStartup;
if (_lastRt > 0f && now - _lastRt > 0.5f && Hangs.Count < 40) Hangs.Add((now - T0).ToString("F1") + "@" + (now - _lastRt).ToString("F1") + "s");
_lastRt = now;
}
void Update()
{
if (!Ready) return;
var pc = MyValue.MyPC as PCActor;
if (pc == null) return;
var cfg = Cfg;
if (cfg.moveKeys != 0)
{
float h = (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow) ? 1f : 0f);
float v = (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow) ? 1f : 0f) - (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow) ? 1f : 0f);
if (h != 0f || v != 0f)
{
if (!_moving) { pc.Set_AutoBraking(false); pc.Set_JoystickStatus(1); _moving = true; }
var cam = Camera.main;
Vector3 fwd = cam != null ? Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up) : Vector3.forward;
if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward;
fwd.Normalize();
pc.Run_byJoystick((fwd * v + Vector3.Cross(Vector3.up, fwd) * h).normalized);
}
else if (_moving) { pc.Set_JoystickStatus(0); pc.Set_AutoBraking(true); _moving = false; }
}
if (Input.GetKeyDown(_kAttack)) { if (Dummy != null) pc.Change_Target(false, Dummy); pc.Play_Attack(0, 1f); _msg = "기본 공격"; }
if (Input.GetKeyDown(_kPrev)) _msg = CycleWeapon(-1);
if (Input.GetKeyDown(_kNext)) _msg = CycleWeapon(+1);
if (Input.GetKeyDown(_kReset)) _msg = ResetDummy();
if (Input.GetKeyDown(_kSlow)) { _slow = !_slow; Time.timeScale = _slow ? Mathf.Clamp(cfg.slowScale, 0.05f, 1f) : 1f; _msg = _slow ? "슬로 모션 ×" + Time.timeScale : "정상 속도"; }
#if UNITY_EDITOR
if (Input.GetKeyDown(_kPause)) UnityEditor.EditorApplication.isPaused = !UnityEditor.EditorApplication.isPaused;
#endif
}
void OnGUI()
{
if (Cfg.label == 0) return;
if (_style == null) { _style = new GUIStyle(GUI.skin.label); _style.alignment = TextAnchor.UpperLeft; _style.wordWrap = true; }
_style.fontSize = Mathf.Max(10, Mathf.RoundToInt(Screen.height * Cfg.labelFontScale));
_style.normal.textColor = Ready ? Color.white : new Color(1f, 0.9f, 0.5f);
string w = "-";
if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; }
string s = "WL 스킬 테스트 · " + Stage + (Ready ? "" : " …") +
"\n무기 " + w + " 더미 HP " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") + (_slow ? " [슬로 ×" + Time.timeScale + "]" : "") +
"\nWASD 이동 · Space 공격 · Q 주력 · E 각성 · 숫자 등급 · [ ] 무기 · R 더미 · T 슬로 · P 일시정지" +
(string.IsNullOrEmpty(_msg) ? "" : "\n" + _msg) + (Ready ? "\n" + string.Join(" · ", Laps.ToArray()) : "");
var r = new Rect(Screen.width * 0.01f, Screen.height * 0.01f, Screen.width * 0.6f, Screen.height * 0.3f);
GUI.color = new Color(0f, 0f, 0f, 0.6f); GUI.Label(new Rect(r.x + 1, r.y + 1, r.width, r.height), s, _style);
GUI.color = Color.white; GUI.Label(r, s, _style);
}
}
}