WL #947 독립 스킬 테스트 하네스(검증 전) — WLSkillTestHarness(MainToStatTest 상속 · 로그인·HUD 없음) · 빌더 재작성 · 원본 StatTest 분기 13줄 → WLTestScene.Active · 구 부트 제거. 씬 재조립·Play 검증은 다음 세션

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-20 01:48:59 +09:00
parent d51fb12ed1
commit dc516cc42e
14 changed files with 412 additions and 591 deletions

View File

@ -535,7 +535,7 @@ public class Actor : MyCoroutine
if (!WL.Combat.Core.CombatEvents.RaiseHitConfirmed(this, _dinfo)) MyValue.m_RealCamera.ShakeCamera(1);
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
{
MainToStatTest.Ins.Set_Dmglabel(HUDBase.Get_TextColor(_dinfo.Beater.m_AttackType) + (_dinfo.Critical == eStat.CRI || _dinfo.Critical == eStat.M_CRI ? "" + _dinfo.Critical + " " : " ") + _dinfo.Damage);
return;
@ -608,7 +608,7 @@ public class Actor : MyCoroutine
}
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
return;
#endif
// UI 설정
@ -643,7 +643,7 @@ public class Actor : MyCoroutine
if (!IsDead()) return; // 부활한 경우
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
{
On_Regen();
transform.localPosition = Vector3.zero;
@ -802,7 +802,7 @@ public class Actor : MyCoroutine
m_Stat.Set_Stat(true, eStat.HP, _hp);
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
{
MainToStatTest.Ins.Set_Dmglabel($"HP : {_hp}");
return;
@ -830,7 +830,7 @@ public class Actor : MyCoroutine
m_Stat.Set_Stat(true, eStat.MP, Math.Abs(_mp));
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
{
MainToStatTest.Ins.Set_Dmglabel($"MP : {_mp}");
return;

View File

@ -121,7 +121,7 @@ public class MobActor : Actor
HPRegenPerSec = (float)m_Stat.Get_Stat(eStat.HP_REGEN_Per1Sec);
#if UNITY_EDITOR
if (SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
{
transform.localPosition = Vector3.zero;
return;

View File

@ -30,7 +30,7 @@ public class WizardActor : PCActor
{
ProjectileInfo.Ins.Shoot_Projectile("MagicMissile", this, m_Target, m_DamageRate, Get_CenterPositionFoward(), 10f);
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest") return;
if (WL.SkillTest.WLTestScene.Active) return;
#endif
//NewGameUI.Ins.m_GuideQuestUI.Add_GuideQuest(ePurpose.JoystickAttack);
}

View File

@ -231,7 +231,7 @@ public class PCActor : MyActor
//}
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest" ||
if (WL.SkillTest.WLTestScene.Active ||
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "Settings")
return;
#endif

View File

@ -354,7 +354,7 @@ public class ProjectileBase : MonoBehaviour
protected List<Actor> Get_Enemies(Transform tf, float dist)
{
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
return new List<Actor> { MainToStatTest.Ins.actors[1] };
#endif
return ActorInfo.Ins.Get_EnemyActors(m_ProjecTileData.shooter.IsEnemy(), tf.position, dist);

View File

@ -58,7 +58,7 @@ public partial class InGameInfo : MyCoroutine
list_projectileData.Add(new ProjectileData());
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest")
if (WL.SkillTest.WLTestScene.Active)
return;
#endif
@ -98,7 +98,7 @@ public partial class InGameInfo : MyCoroutine
}
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest") yield break;
if (WL.SkillTest.WLTestScene.Active) yield break;
#endif
ObscuredCheatingDetector.StartDetection(OnCheaterDetected);

View File

@ -83,7 +83,7 @@ public class ServerInfo : MyCoroutine
private void OnApplicationPause(bool pause)
{
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest") return;
if (WL.SkillTest.WLTestScene.Active) return;
#endif
if (pause)

View File

@ -12,7 +12,7 @@ public class Play3DSound : MonoBehaviour
private void OnEnable()
{
#if UNITY_EDITOR
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest") return;
if (WL.SkillTest.WLTestScene.Active) return;
#endif
if (m_Clip != null)

View File

@ -1,21 +1,22 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLSkillTestSceneBuilder.cs — 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장한다 (PD #947 · 2026-10-19)
// WLSkillTestSceneBuilder.cs — 독립 스킬 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장 (PD #947 · 2026-10-20 재지시판)
//
// ■ 왜 코드로: 씬에 무엇이 들어가는지가 글로 남고, 깨지면 메뉴 한 번으로 다시 만든다(에디터 손작업 0 · 열린 씬(Title)은 건드리지 않는다).
// ■ 들어가는 것 (= Title 이 Addressables 로 올리던 매니저 + InGame 씬 루트 + 원본 카메라 프리팹 + 평지 + 부트)
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 부트가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, UIAtlasMgr, SortOrder_5, ActorInfo}.prefab · ResWork/Prefabs/Ingame/Main Camera.prefab(RealCamera)
// · InGame 씬 루트 4종(IngameTouchInfo · DungeonInfo · DropItemInfo · Global Volume(GlobalVolumeMgr + 꺼진 Volume · 같은 프로필)) · ProjectileInfo
// · WLSkillTestBoot(로그인 뒤 InGameInfo·PCInfo 프리팹 생성)
// ■ 메뉴 WL/Skill Test/Build Scene · CLI: unity command eval --code 'return WL.SkillTest.Editor.WLSkillTestSceneBuilder.Build();'
// ■ 들어가는 것(= 원본 StatTest 하네스 구성 + WL 치비·더미·평지) — 로그인·HUD·섬 없음
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 하네스가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, SortOrder_5(OptionInfo·Popup), ActorInfo}.prefab
// · ResWork/Prefabs/Ingame/{InGameInfo(이펙트·투사체 풀), ProjectileInfo, Main Camera(RealCamera)}.prefab
// · PCPos/치비 PC 프리팹(WLCharacterSwapSettings.pcPrefabName · 없으면 Ai01) · MobPos/더미 몹 프리팹(Res_Addr/Mobs/WL/RPG_Bat)
// · Canvas + 글자 4(원본 Set_Dmglabel 이 쓰는 피해 로그) · WLSkillTestHarness(actors · labels · pcActor · dummyActor)
// ■ 메뉴 WL/Skill Test/Build Scene · CLI: 먼저 CloseTestScene() 또는 Title 을 연 뒤 Build()
// ─────────────────────────────────────────────────────────────────────────────
using System.Text;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace WL.SkillTest.Editor
{
@ -25,13 +26,12 @@ namespace WL.SkillTest.Editor
public const string ScenePath = Folder + "/WLSkillTest.unity";
public const string GroundMatPath = Folder + "/WLSkillTestGround.mat";
static readonly string[] TitlePrefabs = { "Tables", "SoundInfo", "UIAtlasMgr", "SortOrder_5", "ActorInfo" };
static readonly string[] TitlePrefabs = { "Tables", "SoundInfo", "SortOrder_5", "ActorInfo" };
const string TitlePrefabFolder = "Assets/ResWork/UIPrefabs/Title/";
const string MainCameraPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string InGameInfoPrefab = "Assets/ResWork/Prefabs/Ingame/InGameInfo.prefab";
const string PCInfoPrefab = "Assets/ResWork/Prefabs/Ingame/PCInfo.prefab";
const string ProjectileInfoPrefab = "Assets/ResWork/Prefabs/Ingame/ProjectileInfo.prefab";
const string VolumeProfileGuid = "96e4f5994a1f2d948a293a2d5f2bd1b3"; // InGame.unity 의 Global Volume 프로필(실측)
static readonly string[] IngamePrefabs = { "InGameInfo", "ProjectileInfo", "Main Camera" };
const string IngamePrefabFolder = "Assets/ResWork/Prefabs/Ingame/";
const string PcPrefabFolder = "Assets/Res_Addr/PC/";
const string DummyPrefab = "Assets/Res_Addr/Mobs/WL/RPG_Bat.prefab";
[MenuItem("WL/Skill Test/Build Scene (다시 만들기)")]
public static void BuildMenu() { Debug.Log(Build()); }
@ -39,10 +39,10 @@ namespace WL.SkillTest.Editor
[MenuItem("WL/Skill Test/Open Scene")]
public static void OpenMenu() { Debug.Log(Open()); }
/// <summary>테스트 씬을 연다(Single). 현재 씬이 저장 안 된 상태면 열지 않고 알린다(K-WL-114).</summary>
public static string Open()
{
var cur = SceneManager.GetActiveScene();
if (cur.path == ScenePath) return "열림 " + ScenePath;
if (cur.isDirty) return "현재 씬 " + cur.name + " 이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤 다시";
if (!System.IO.File.Exists(ScenePath)) return "씬 없음 " + ScenePath + " — Build() 먼저";
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
@ -55,100 +55,115 @@ namespace WL.SkillTest.Editor
return false;
}
/// <summary>테스트 씬이 열려 있으면 **저장 없이** 빈 씬으로 바꾼다(다시 만들기 전 · 저장 안 된 변경이 있으면 거절).</summary>
/// <summary>테스트 씬이 열려 있으면 저장 없이 빈 씬으로(다시 만들기 전 · 저장 안 된 변경이 있으면 거절).</summary>
public static string CloseTestScene()
{
if (Application.isPlaying) return "Play 중";
if (!IsTestSceneOpen()) return "테스트 씬 안 열림";
var a = SceneManager.GetActiveScene();
if (a.path == ScenePath && a.isDirty) return "테스트 씬이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤";
if (a.path == ScenePath && a.isDirty) return "테스트 씬이 저장되지 않음";
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
return "빈 씬으로 바꿈";
}
static string PcPrefabPath()
{
var s = WL.Character.WLCharacterSwapSettings.Instance;
string name = s != null && !string.IsNullOrEmpty(s.pcPrefabName) ? s.pcPrefabName : "Ai01";
string p = PcPrefabFolder + name + ".prefab";
if (AssetDatabase.LoadAssetAtPath<GameObject>(p) == null) p = PcPrefabFolder + "Ai01.prefab";
return p;
}
static TextMeshProUGUI MakeLabel(Transform canvas, string name, Vector2 anchor, Vector2 pos, Vector2 size, TextAlignmentOptions align)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(canvas, false);
var rt = go.GetComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = rt.pivot = anchor; rt.anchoredPosition = pos; rt.sizeDelta = size;
var t = go.AddComponent<TextMeshProUGUI>();
t.fontSize = 16; t.alignment = align; t.color = Color.white; t.text = "";
return t;
}
public static string Build()
{
if (Application.isPlaying) return "Play 중에는 만들지 않는다";
if (IsTestSceneOpen()) return "테스트 씬이 열려 있음 — 먼저 CloseTestScene() 또는 Title 을 연 뒤 Build()";
var sb = new StringBuilder();
var prevActive = SceneManager.GetActiveScene();
if (!AssetDatabase.IsValidFolder(Folder)) { AssetDatabase.CreateFolder("Assets/WL", "SkillTest"); }
if (!AssetDatabase.IsValidFolder(Folder)) AssetDatabase.CreateFolder("Assets/WL", "SkillTest");
var cfg = WLSkillTestBoot.Cfg;
// 테스트 씬이 열려 있으면 그 경로로 저장할 수 없다(Unity: "Overwriting the same path as another open scene is not allowed")
// → 같은 호출 안에서 씬을 바꾸면 파이프라인 실행이 끊긴다(실측) · 먼저 CloseTestScene() 을 따로 부른 뒤 Build()
if (IsTestSceneOpen()) return "테스트 씬이 열려 있음 — 먼저 CloseTestScene()(빈 씬으로) 을 부른 뒤 Build()";
var cfg = WLSkillTestHarness.Cfg;
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
SceneManager.SetActiveScene(scene);
// 빛
var lightGo = new GameObject("Directional Light");
var light = lightGo.AddComponent<Light>();
light.type = LightType.Directional; light.intensity = 1.1f; light.shadows = LightShadows.Soft; light.color = Color.white;
light.type = LightType.Directional; light.intensity = 1.1f; light.shadows = LightShadows.Soft;
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
// 평지(콜라이더 = NavMesh 베이크 기준)
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.name = "Ground";
float size = cfg != null && cfg.groundSize > 1f ? cfg.groundSize : 60f;
float size = cfg != null && cfg.groundSize > 1f ? cfg.groundSize : 100f;
ground.transform.localScale = new Vector3(size / 10f, 1f, size / 10f);
var mat = AssetDatabase.LoadAssetAtPath<Material>(GroundMatPath);
if (mat == null)
{
var sh = Shader.Find("Universal Render Pipeline/Lit");
mat = new Material(sh != null ? sh : Shader.Find("Standard"));
AssetDatabase.CreateAsset(mat, GroundMatPath);
}
if (mat == null) { var sh = Shader.Find("Universal Render Pipeline/Lit"); mat = new Material(sh != null ? sh : Shader.Find("Standard")); AssetDatabase.CreateAsset(mat, GroundMatPath); }
var gc = cfg != null && cfg.groundColor != null && cfg.groundColor.Length >= 3 ? new Color(cfg.groundColor[0], cfg.groundColor[1], cfg.groundColor[2]) : new Color(0.45f, 0.62f, 0.35f);
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", gc); else mat.color = gc;
EditorUtility.SetDirty(mat);
ground.GetComponent<Renderer>().sharedMaterial = mat;
GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); // NavMesh 는 부트가 콜라이더 기준 런타임 베이크
GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic);
// 매니저(Title 의 Mgr 과 같은 구성)
var mgr = new GameObject("Mgr");
mgr.AddComponent<AddrResourceMgr>(); mgr.AddComponent<CoroutineMgr>(); mgr.AddComponent<TimeMgr>();
var es = new GameObject("EventSystem");
es.AddComponent<EventSystem>(); es.AddComponent<StandaloneInputModule>();
var mgr = new GameObject("Mgr"); mgr.AddComponent<AddrResourceMgr>(); mgr.AddComponent<CoroutineMgr>(); mgr.AddComponent<TimeMgr>();
var es = new GameObject("EventSystem"); es.AddComponent<EventSystem>(); es.AddComponent<StandaloneInputModule>();
// Title 이 Addressables 로 올리던 프리팹 → 씬에 직접
foreach (var n in TitlePrefabs)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(TitlePrefabFolder + n + ".prefab");
if (p == null) { sb.Append("🔴 프리팹 없음 ").Append(n).Append('\n'); continue; }
var go = PrefabUtility.InstantiatePrefab(p, scene) as GameObject;
if (go != null) go.name = n;
var go = PrefabUtility.InstantiatePrefab(p, scene) as GameObject; if (go != null) go.name = n;
}
foreach (var n in IngamePrefabs)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(IngamePrefabFolder + n + ".prefab");
if (p == null) { sb.Append("🔴 프리팹 없음 ").Append(n).Append('\n'); continue; }
var go = PrefabUtility.InstantiatePrefab(p, scene) as GameObject; if (go != null) go.name = n;
}
// 원본 인게임 카메라(RealCamera · 태그 MainCamera)
var camP = AssetDatabase.LoadAssetAtPath<GameObject>(MainCameraPrefab);
if (camP != null) { var cam = PrefabUtility.InstantiatePrefab(camP, scene) as GameObject; if (cam != null) cam.name = "Main Camera"; }
else sb.Append("🔴 Main Camera 프리팹 없음\n");
// InGame 씬 루트 4종
new GameObject("IngameTouchInfo").AddComponent<InGameTouchInfo>();
new GameObject("DungeonInfo").AddComponent<DungeonInfo>();
new GameObject("DropItemInfo").AddComponent<DropItemInfo>();
var gv = new GameObject("Global Volume");
gv.AddComponent<GlobalVolumeMgr>();
var vol = gv.AddComponent<Volume>();
vol.isGlobal = true; vol.enabled = false; // InGame.unity 실측: Volume 컴포넌트 꺼짐
var profPath = AssetDatabase.GUIDToAssetPath(VolumeProfileGuid);
if (!string.IsNullOrEmpty(profPath)) vol.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(profPath);
// ProjectileInfo 는 씬에 두지 않는다 — 부트가 InGameInfo 뒤에 원본 프리팹으로 만든다(1차 Play: 예열 투사체가 InGameInfo.Ins null 로 NRE 127건)
// PC(치비) · 더미 — 씬에 직접(StatTest 의 PCPos/MobPos 와 같은 꼴)
Vector3 spawn = cfg != null && cfg.spawnPos != null && cfg.spawnPos.Length >= 3 ? new Vector3(cfg.spawnPos[0], cfg.spawnPos[1], cfg.spawnPos[2]) : Vector3.zero;
var pcPos = new GameObject("PCPos"); pcPos.transform.position = spawn;
var pcPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PcPrefabPath());
Actor pcActor = null;
if (pcPrefab != null) { var pc = PrefabUtility.InstantiatePrefab(pcPrefab, scene) as GameObject; pc.transform.SetParent(pcPos.transform, false); pcActor = pc.GetComponent<Actor>(); }
else sb.Append("🔴 PC 프리팹 없음\n");
var mobPos = new GameObject("MobPos"); mobPos.transform.position = spawn + new Vector3(2f, 0f, -1.5f);
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(DummyPrefab);
MobActor mob = null;
if (mobPrefab != null) { var m = PrefabUtility.InstantiatePrefab(mobPrefab, scene) as GameObject; m.transform.SetParent(mobPos.transform, false); mob = m.GetComponent<MobActor>(); }
else sb.Append("🔴 더미 프리팹 없음 " + DummyPrefab + "\n");
if (pcActor == null) sb.Append("🔴 PC 프리팹에 Actor 없음\n");
if (mob == null) sb.Append("🔴 더미 프리팹에 MobActor 없음\n");
// 부트
var bootGo = new GameObject("WLSkillTestBoot");
var boot = bootGo.AddComponent<WLSkillTestBoot>();
boot.inGameInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(InGameInfoPrefab);
boot.pcInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PCInfoPrefab);
boot.projectileInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(ProjectileInfoPrefab);
if (boot.inGameInfoPrefab == null) sb.Append("🔴 InGameInfo 프리팹 없음\n");
if (boot.pcInfoPrefab == null) sb.Append("🔴 PCInfo 프리팹 없음\n");
if (boot.projectileInfoPrefab == null) sb.Append("🔴 ProjectileInfo 프리팹 없음\n");
// 글자 4(원본 분기 Set_Dmglabel 이 labels[2] 에 피해를 적는다)
var canvasGo = new GameObject("WLSkillTestCanvas");
var canvas = canvasGo.AddComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 50;
canvasGo.AddComponent<CanvasScaler>(); canvasGo.AddComponent<GraphicRaycaster>();
var labels = new TextMeshProUGUI[4];
labels[0] = MakeLabel(canvasGo.transform, "pcstat", new Vector2(0f, 0f), new Vector2(10f, 10f), new Vector2(360f, 220f), TextAlignmentOptions.BottomLeft);
labels[1] = MakeLabel(canvasGo.transform, "mobstat", new Vector2(1f, 0f), new Vector2(-10f, 10f), new Vector2(360f, 220f), TextAlignmentOptions.BottomRight);
labels[2] = MakeLabel(canvasGo.transform, "dmg", new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(420f, 300f), TextAlignmentOptions.TopRight);
labels[3] = MakeLabel(canvasGo.transform, "classinfo", new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(600f, 40f), TextAlignmentOptions.Bottom);
var hGo = new GameObject("WLSkillTestHarness");
var h = hGo.AddComponent<WLSkillTestHarness>();
h.pcActor = pcActor; h.dummyActor = mob;
h.actors = new Actor[] { pcActor, mob };
h.labels = labels;
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append('\n');
// 열려 있던 씬으로 되돌리고 새 씬은 닫는다(Title 무변경)
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append(" · PC ").Append(PcPrefabPath()).Append('\n');
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
EditorSceneManager.CloseScene(scene, true);
AssetDatabase.SaveAssets();

View File

@ -1,28 +1,23 @@
{
"note": "WL 스킬 테스트 씬(PD #947 · 2026-10-19) 값. loginId = 개발 로그인 ID(useOptionUserId 1 이면 Title 에서 마지막에 쓴 ID 우선) · mapId 0 = 섬 행(900 · Lobby 모드 · 씬은 로드하지 않음) · dummyMobId 0 = WLIslandMobSettings.hitTestDummyMobId · dummyOffset 전부 0 = WLIslandMobSettings.hitTestDummyOffset · 키 이름 = UnityEngine.KeyCode",
"loginId": "TestID",
"useOptionUserId": 1,
"mapId": 0,
"note": "독립 스킬 테스트 씬(PD #947 · 2026-10-20) 값. baseClassId = 낀 WL 무기가 없을 때 PC 직업 · autoBattle 1 = 섬처럼 자동 전투 · dummyMobId 0 = WLIslandMobSettings.hitTestDummyMobId · dummyOffset 전부 0 = WLIslandMobSettings.hitTestDummyOffset · 키 이름 = UnityEngine.KeyCode",
"baseClassId": 10101,
"autoBattle": 1,
"spawnPos": [0, 0, 0],
"cameraRig": 1,
"dummyMobId": 0,
"dummyCount": 1,
"dummySpawnerId": 1,
"dummyOffset": [0, 0, 0],
"dummyGap": 2.5,
"waitPcSeconds": 30,
"forceSpawn": 1,
"probeAtlas": 0,
"probeNoLogin": 0,
"probeProfiler": 0,
"dummyStill": 1,
"skyColor": [0.55, 0.75, 0.95],
"moveKeys": 1,
"attackKey": "Space",
"weaponPrevKey": "LeftBracket",
"weaponNextKey": "RightBracket",
"dummyResetKey": "R",
"slowKey": "T",
"slowScale": 0.2,
"pauseKey": "P",
"label": 1,
"labelFontScale": 0.018,
"groundSize": 100,
"groundColor": [0.45, 0.62, 0.35]
"groundColor": [0.45, 0.62, 0.35],
"skyColor": [0.55, 0.75, 0.95]
}

View File

@ -1,493 +0,0 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLSkillTestBoot.cs — 무기 스킬 구현용 가벼운 테스트 씬(WLSkillTest) 부트 (PD #947 · 2026-10-19)
//
// PD 원문: 「Title 씬에서 무기 스킬 작업을 진행하다보니 쓸데 없이 로딩이 너무 길고 테스트가 힘들어. … 최대한 가벼운 테스트용 씬을 만들고
// 이 씬에서 스킬 작업을 할 수 있도록 … (우리 게임의 무기 스킬 구현은 앞으로 테스트 씬을 통해 진행할게)」
//
// ■ 왜 — Title → 로그인 → Ingame → 섬 흐름은 Play 1회에 약 115 s(도메인 리로드 32 + Title 씬 21 + Title 초기화 47 + 로그인 3.5 + 섬 9 · #943-① 실측).
// 스킬 작업은 PC 1 + 무적 더미 1 + 평지면 충분하다 → Title 씬·섬 씬(FarmingIsland)·농장 시스템을 빼고 **원본 InGame 흐름만** 돌린다.
// ■ 어떻게 (원본 코드 0줄 · 테이블 0줄 · 빌드 설정 0줄)
// ① 씬에 매니저 프리팹을 직접 둔다(Title 이 Addressables 로 올리던 Tables·SoundInfo·UIAtlasMgr·SortOrder_5·ActorInfo + Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr)
// + EventSystem + 원본 Main Camera 프리팹(RealCamera) + InGame 씬 루트 4종) → 테이블 로딩 대기. ※ 씬 조립 = Editor/WLSkillTestSceneBuilder.cs
// ② 개발 로그인(ServerInfo.Login · TitleInfo 없이 · ID = PlayerPrefs Option_UserID 또는 설정값) → m_ServerData(진짜 계정 데이터 · 장비·스킬 슬롯 그대로).
// ③ 섬·던전과 같은 LoadMapMgr 대체 노드(WLIslandMapNodes)에 시작 지점 + 섬 구도 카메라 리그(WLDungeonCameraRig).
// ④ InGameInfo·PCInfo 프리팹을 **로그인 뒤** 생성 → InGameInfo 가 NewGameUI(HUD · 조이스틱 · 장비창)를 올리고 NewGameUI.Start 가 Load_Map(섬 900)을 부른다.
// 그 전에 SceneInfo.CurStageData 를 900 행으로 미리 채워(리플렉션) 「같은 맵」 분기 = **섬 씬을 로드하지 않고** PCInfo.Make_Actors 만 돈다.
// ⑤ PC 가 생기면 StartCam + SetUI_byGameMode(Lobby) + 무적 더미(몬스터 표 hitTestDummyMobId · WLTrailTest 채택 · HP 되채움) → Stage = ready.
// ⑥ 키: WASD/방향키 이동(원본 Run_byJoystick) · [ ] 낀 무기 바꾸기(WLInventory 장비 순환) · R 더미 제자리 · Q/E/숫자 = 기존 훅(WLSkillHotkey·WLWeaponGradeCheat).
// ■ 값 = Resources/WL/WLSkillTest.json (C45) · 진단 = Describe() · 단계 시각 = Laps(로딩 실측 보고용)
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다) · 씬 이름 "WLSkillTest" 는 원본 "StatTest" 분기와 무관(그 분기는 MainToStatTest 를 요구한다).
// 🔴 섬 씬 이름(Level01)과 다르므로 WLIslandBridge·WLIslandMobZones 등 섬 전용 훅은 이 씬에서 돌지 않는다(의도).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using WL.Combat.Weapon;
using WL.Equip;
using WL.Island;
using WL.IslandMob;
namespace WL.SkillTest
{
[System.Serializable]
public class WLSkillTestJson
{
public string note = "";
public string loginId = "TestID"; // 개발 로그인 ID 기본값
public int useOptionUserId = 1; // 1 = PlayerPrefs Option_UserID(Title 에서 마지막에 쓴 ID)를 우선
public int mapId = 0; // 0 = WLIslandSettings.islandMapId(섬 행 900 · Lobby 모드) — 씬은 로드하지 않고 모드·노드만 빌린다
public float[] spawnPos = { 0f, 0f, 0f };
public int cameraRig = 1; // 1 = 섬 구도 카메라 리그(WLDungeonCameraRig) 생성
public int dummyMobId = 0; // 0 = WLIslandMobSettings.hitTestDummyMobId
public int dummyCount = 1;
public int dummySpawnerId = 1;
public float[] dummyOffset = { 0f, 0f, 0f }; // 전부 0 = WLIslandMobSettings.hitTestDummyOffset (PC 기준 오른쪽·위·앞)
public float dummyGap = 2.5f; // 더미가 둘 이상일 때 옆 간격 m
public float waitPcSeconds = 30f;
public int forceSpawn = 1; // 1 = PC 가 생긴 뒤 시작 지점(spawnPos)으로 한 번 더 워프
public int probeAtlas = 0; // 진단: 1 = 부트 첫머리에서 UI 아틀라스 로드 시간을 잰다(평소 0)
public int probeNoLogin = 0; // 진단: 1 = 로그인 없이 60 s 프레임 멈춤만 기록(평소 0)
public int probeProfiler = 0; // 진단: 1 = 프로파일러를 켜고 3 s 넘는 프레임의 무거운 호출 경로를 ProfileDump 에(평소 0 · probeNoLogin 과 함께)
public int dummyStill = 1; // 1 = 더미가 제자리(Stop) · 0 = 섬처럼 쫓아옴
public float[] skyColor = { 0.55f, 0.75f, 0.95f }; // 스카이박스가 없을 때 카메라 배경색(빈 값이면 손대지 않음)
public int moveKeys = 1; // WASD · 방향키 이동
public string weaponPrevKey = "LeftBracket";
public string weaponNextKey = "RightBracket";
public string dummyResetKey = "R";
public int label = 1; // 화면 왼쪽 위 상태 글자
public float labelFontScale = 0.018f;
public float groundSize = 60f; // 평지 한 변 m(빌더가 읽는다)
public float[] groundColor = { 0.45f, 0.62f, 0.35f };
}
public sealed class WLSkillTestBoot : MonoBehaviour
{
public const string SceneName = "WLSkillTest";
public const string ResourcesPath = "WL/WLSkillTest";
[Header("씬 빌더가 채운다 — 로그인 뒤에 생성하는 프리팹(InGameInfo → ProjectileInfo → PCInfo 순 · 투사체 예열이 InGameInfo.Ins 를 쓴다)")]
public GameObject inGameInfoPrefab;
public GameObject pcInfoPrefab;
public GameObject projectileInfoPrefab;
public static WLSkillTestBoot Ins;
public static string Stage = "none";
public static string LastLog = "";
public static bool Ready;
public static Actor Dummy;
public static readonly List<string> Laps = new List<string>();
/// <summary>PlayFab 호출 시각 기록(에디터 · 로딩 실측용 · 2차 Play 에서 로그인 52 s 의 내역을 보려고)</summary>
public static readonly List<string> PfLog = new List<string>();
/// <summary>0.5 s 넘게 멈춘 프레임 기록(시각@길이 · 메인 스레드가 무엇에 막혔는지 PfLog·Laps 와 맞춰 본다)</summary>
public static readonly List<string> Hangs = new List<string>();
public static float T0;
float _lastRt;
void LateUpdate()
{
float now = Time.realtimeSinceStartup;
if (_lastRt > 0f && now - _lastRt > 0.5f && Hangs.Count < 40)
{
string flags = "";
#if UNITY_EDITOR
flags = " L" + (UnityEditor.Lightmapping.isRunning ? 1 : 0) + "S" + (UnityEditor.ShaderUtil.anythingCompiling ? 1 : 0) + "C" + (UnityEditor.EditorApplication.isCompiling ? 1 : 0) + "U" + (UnityEditor.EditorApplication.isUpdating ? 1 : 0);
#endif
Hangs.Add((now - T0).ToString("F1") + "@" + (now - _lastRt).ToString("F1") + "s" + flags);
#if UNITY_EDITOR
// 멈춤 직후 그 프레임(과 직전 프레임)의 가장 무거운 호출 경로를 바로 적는다 — 프레임 버퍼가 밀려 사라지기 전에
if (Cfg.probeProfiler != 0) { int li = UnityEditorInternal.ProfilerDriver.lastFrameIndex; DumpFrame(li - 1, 1000f); DumpFrame(li, 1000f); }
#endif
}
_lastRt = now;
}
#if UNITY_EDITOR
/// <summary>진단: 프로파일러로 잡은 긴 프레임의 가장 무거운 호출 경로(멈춤의 정체)</summary>
public static string ProfileDump = "";
static void ProfilerStart() { s_prof.Length = 0; ProfileDump = ""; UnityEditorInternal.ProfilerDriver.ClearAllFrames(); UnityEditorInternal.ProfilerDriver.enabled = true; }
static readonly System.Text.StringBuilder s_prof = new System.Text.StringBuilder();
static void DumpFrame(int f, float minMs)
{
if (f < 0) return;
int col = UnityEditor.Profiling.HierarchyFrameDataView.columnTotalTime;
var kids = new List<int>();
using (var v = UnityEditorInternal.ProfilerDriver.GetHierarchyFrameDataView(f, 0, UnityEditor.Profiling.HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName, col, false))
{
if (v == null || !v.valid || v.frameTimeMs < minMs) return;
s_prof.Append(" || frame ").Append(f).Append(' ').Append(v.frameTimeMs.ToString("F0")).Append("ms:");
int id = v.GetRootItemID();
for (int depth = 0; depth < 12; depth++)
{
kids.Clear(); v.GetItemChildren(id, kids);
if (kids.Count == 0) break;
int best = kids[0]; float bestMs = -1f, second = 0f; string secondName = "";
foreach (var k in kids) { float ms = v.GetItemColumnDataAsFloat(k, col); if (ms > bestMs) { if (bestMs > second) { second = bestMs; secondName = v.GetItemName(best); } bestMs = ms; best = k; } else if (ms > second) { second = ms; secondName = v.GetItemName(k); } }
s_prof.Append(" > ").Append(v.GetItemName(best)).Append(' ').Append(bestMs.ToString("F0"));
if (second > 1000f) s_prof.Append(" (+").Append(secondName).Append(' ').Append(second.ToString("F0")).Append(')');
id = best;
}
}
ProfileDump = s_prof.ToString();
}
static void ProfilerDump(float minMs)
{
int first = UnityEditorInternal.ProfilerDriver.firstFrameIndex, last = UnityEditorInternal.ProfilerDriver.lastFrameIndex;
s_prof.Append(" || buffer ").Append(first).Append("~").Append(last);
for (int f = first; f <= last && f >= 0; f++) DumpFrame(f, minMs);
ProfileDump = s_prof.ToString();
UnityEditorInternal.ProfilerDriver.enabled = false;
}
#endif
static System.Reflection.FieldInfo s_fAtlas;
static bool AtlasReady()
{
if (!UIAtlasMgr.isIns || UIAtlasMgr.Ins == null) return false;
if (s_fAtlas == null) s_fAtlas = typeof(UIAtlasMgr).GetField("UIAtlas", BindingFlags.NonPublic | BindingFlags.Instance);
return s_fAtlas != null && s_fAtlas.GetValue(UIAtlasMgr.Ins) != null;
}
#if UNITY_EDITOR
static void OnPf(PlayFab.Internal.ApiProcessingEventArgs e) { PfLog.Add((Time.realtimeSinceStartup - T0).ToString("F1") + " " + e.EventType + " " + e.ApiEndpoint); }
static void OnPfErr(PlayFab.SharedModels.PlayFabRequestCommon r, PlayFab.PlayFabError err) { PfLog.Add((Time.realtimeSinceStartup - T0).ToString("F1") + " ERR " + (err != null ? err.Error + " " + err.ErrorMessage : "?")); }
#endif
static WLSkillTestJson s_cfg;
bool _moving;
KeyCode _kPrev = KeyCode.LeftBracket, _kNext = KeyCode.RightBracket, _kReset = KeyCode.R;
string _weaponMsg = "";
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 bool Active { get { return SceneManager.GetActiveScene().name == SceneName; } }
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; }
/// <summary>상태 파일(Library/ 아래 · 미추적) — 외부 스크립트는 Play 중 `eval` 대신 이 파일을 읽는다. 실측(8차 Play): Play 뒤 첫 `unity command eval/run_script` 가 EditorLoop 에서 약 35 s 메인 스레드를 멈춘다(스크립트 컴파일 준비).</summary>
public static string StatusPath { get { return System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, "../Library/WLSkillTest_status.txt")); } }
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();
}
void Awake()
{
Ins = this;
T0 = Time.realtimeSinceStartup;
Laps.Clear(); PfLog.Clear(); Hangs.Clear(); Ready = false; Dummy = null; s_cfg = null;
#if UNITY_EDITOR
if (Cfg.probeProfiler != 0) ProfilerStart();
PlayFab.Internal.PlayFabHttp.ApiProcessingEventHandler -= OnPf; PlayFab.Internal.PlayFabHttp.ApiProcessingEventHandler += OnPf;
PlayFab.Internal.PlayFabHttp.ApiProcessingErrorEventHandler -= OnPfErr; PlayFab.Internal.PlayFabHttp.ApiProcessingErrorEventHandler += OnPfErr;
#endif
var cfg = Cfg;
_kPrev = Key(cfg.weaponPrevKey, KeyCode.LeftBracket);
_kNext = Key(cfg.weaponNextKey, KeyCode.RightBracket);
_kReset = Key(cfg.dummyResetKey, KeyCode.R);
Lap("awake");
}
IEnumerator Start()
{
var cfg = Cfg;
#if UNITY_EDITOR
// 진단(probeAtlas 1): UI 스프라이트 아틀라스(.spriteatlasv2)를 에디터 API 로 직접 읽어 걸리는 시간을 잰다 — 4차 Play 실측에서 로그인 중 메인 스레드가 16 s + 34 s 멈췄고 첫 멈춤 끝 = 아틀라스 도착 시각
if (cfg.probeAtlas != 0)
{
float a = Time.realtimeSinceStartup;
var atl = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.U2D.SpriteAtlas>("Assets/Res_Addr/MainUI/UIAtlas.spriteatlasv2");
float b = Time.realtimeSinceStartup;
var sp = atl != null ? atl.GetSprite("grade1") : null;
float c = Time.realtimeSinceStartup;
Laps.Add("atlasLoad " + (b - a).ToString("F1") + "s getSprite " + (c - b).ToString("F1") + "s spr=" + (sp != null) + " n=" + (atl != null ? atl.spriteCount : -1));
}
#endif
// ⓪ 평지 NavMesh — 콜라이더 기준 런타임 베이크(WLDungeonSceneSetup 과 같은 방식 · 임포트 설정 0) + 배경색
BakeGround();
ApplySky(cfg);
Lap("navmesh");
// ⓪-2 Addressables 초기화를 **따로 재서** 보이게 한다(Use Asset Database 모드의 카탈로그 준비 · 어차피 첫 로드 때 든다)
var init = UnityEngine.AddressableAssets.Addressables.InitializeAsync();
while (!init.IsDone) yield return null;
Lap("addr-init");
// ① 테이블(Tables 프리팹 = TableChecker + 표 44개)
while (!TableChecker.isIns || TableChecker.Ins == null || !TableChecker.Ins.CheckAllLoad()) yield return null;
Lap("tables");
// 진단(probeNoLogin 1): 로그인 없이 60 s 동안 프레임 멈춤만 기록하고 멈춘다 — 멈춤이 PlayFab 때문인지 에디터(조명·셰이더) 때문인지 가른다
if (cfg.probeNoLogin != 0)
{
float w = 0f; while (w < 60f) { w += Time.unscaledDeltaTime; yield return null; }
#if UNITY_EDITOR
if (cfg.probeProfiler != 0) ProfilerDump(3000f);
#endif
Stage = "nologin-done"; LastLog = "로그인 없이 60 s 관찰"; WriteStatus(); yield break;
}
// ② 개발 로그인 — TitleInfo.OnClick_DevLogin 과 같은 순서(ID 저장 → ServerInfo.Login)
var si = ServerInfo.Ins;
string id = cfg.useOptionUserId != 0 ? PlayerPrefs.GetString(MyValue.OptionKey_UserID, cfg.loginId) : cfg.loginId;
if (string.IsNullOrEmpty(id)) id = string.IsNullOrEmpty(cfg.loginId) ? "TestID" : cfg.loginId;
PlayerPrefs.SetString(MyValue.OptionKey_UserID, id);
si.UserID = id;
bool done = false, fail = false, atlasLapped = false;
si.Login(newuser => { done = true; }, () => { fail = true; });
while (!done && !fail)
{
if (!atlasLapped && AtlasReady()) { atlasLapped = true; Lap("atlas"); Stage = "login"; } // UIAtlasMgr 의 스프라이트 아틀라스(에디터 로드)가 끝난 시각
yield return null;
}
if (!atlasLapped && AtlasReady()) Lap("atlas");
if (fail || si.m_ServerData == null) { Stage = "login-fail"; LastLog = "로그인 실패 — ID " + id; WriteStatus(); yield break; }
Lap("login " + id);
// ③ 맵 노드(LoadMapMgr 대체) + 카메라 리그 — PCInfo.Make_Actors 보다 먼저 있어야 한다(같은 프레임)
var icfg = WLIslandSettings.Instance;
int mapId = cfg.mapId > 0 ? cfg.mapId : (icfg != null ? icfg.islandMapId : 900);
MyValue.MyChoiceMapData.n_MapID = mapId;
MyValue.MyChoiceMapData.n_PortalIndex = 0;
Vector3 spawn = V3(cfg.spawnPos);
float yaw = icfg != null ? WLDungeonCameraRig.SolvedYaw(icfg) : 0f;
if (icfg != null)
{
var nodes = WLIslandMapNodes.Ensure(icfg);
if (nodes != null) nodes.SetStartPos(spawn, yaw);
if (cfg.cameraRig != 0 && Object.FindFirstObjectByType<WLDungeonCameraRig>() == null)
WLDungeonCameraRig.Create(icfg, gameObject.scene);
}
// ④ InGame 매니저 — NewGameUI.Start 의 Load_Map(mapId) 이 「같은 맵」 분기를 타도록 SceneInfo.CurStageData 를 미리 채운다
if (inGameInfoPrefab != null && InGameInfo.Ins == null) Instantiate(inGameInfoPrefab).name = "InGameInfo";
if (projectileInfoPrefab != null && !ProjectileInfo.isIns) Instantiate(projectileInfoPrefab).name = "ProjectileInfo"; // 🔴 InGameInfo 뒤 — 예열 투사체 OnEnable → Off → Show_HitEffect 가 InGameInfo.Ins 를 쓴다(1차 Play 실측 NRE 127건)
if (pcInfoPrefab != null && !PCInfo.isIns) Instantiate(pcInfoPrefab).name = "PCInfo";
var row = table_battlemapconfig.Ins.Get_Data(mapId);
var sinfo = InGameInfo.Ins != null ? InGameInfo.Ins.m_SceneInfo : null;
if (row == null) { Stage = "map-row-none"; LastLog = "BattleMapConfig 행 없음 " + mapId; WriteStatus(); yield break; }
if (sinfo == null) { Stage = "sceneinfo-none"; LastLog = "InGameInfo.m_SceneInfo 없음"; WriteStatus(); yield break; }
var f = typeof(SceneInfo).GetField("CurStageData", BindingFlags.NonPublic | BindingFlags.Instance);
if (f == null) { Stage = "reflect-fail"; LastLog = "SceneInfo.CurStageData 필드 없음"; WriteStatus(); yield break; }
f.SetValue(sinfo, row);
Lap("ingame");
// ⑤ PC — NewGameUI(HUD) 로드 → Load_Map → Make_Actors(Addressables) → PCActor.Set → MyValue.MyPC
float t = 0f;
while (t < cfg.waitPcSeconds && !(MyValue.bMyPC && MyValue.MyPC != null)) { t += Time.unscaledDeltaTime; yield return null; }
if (!(MyValue.bMyPC && MyValue.MyPC != null)) { Stage = "pc-timeout"; LastLog = "PC 생성 대기 초과 " + cfg.waitPcSeconds + "s · NewGameUI " + NewGameUI.isIns; WriteStatus(); yield break; }
Lap("pc");
yield return null;
// 시작 지점 보정 — 1차 Play 실측: PC 가 평지 모서리(28, -29)에 서 있었다(원인 미확정) → NavMesh 위 시작 지점으로 한 번 더 워프
if (cfg.forceSpawn != 0) { var pcA = MyValue.MyPC; pcA.Stop(true); pcA.Set_Warp(NavPos(spawn)); }
if (InGameInfo.Ins != null) InGameInfo.Ins.StartCam();
t = 0f;
while (t < 10f && !NewGameUI.isIns) { t += Time.unscaledDeltaTime; yield return null; }
if (NewGameUI.isIns && NewGameUI.Ins != null) NewGameUI.Ins.SetUI_byGameMode(eGameMode.Lobby); // 로딩 화면 닫기 + 섬과 같은 로비 UI
Lap("ui");
// ⑥ 무적 더미
yield return StartCoroutine(Co_SpawnDummies(cfg));
Ready = true;
LastLog = "";
Lap("ready");
}
void OnDestroy() { try { if (System.IO.File.Exists(StatusPath)) System.IO.File.Delete(StatusPath); } catch { } }
// ─────────────────────────────────────────────────────────────────
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 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();
}
IEnumerator Co_SpawnDummies(WLSkillTestJson cfg)
{
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) { LastLog = "몬스터 표 없음 " + mobId; yield break; }
Vector3 off = V3(cfg.dummyOffset);
if (off == Vector3.zero && ms != null) off = ms.hitTestDummyOffset;
var pc = MyValue.MyPC;
int want = Mathf.Max(1, cfg.dummyCount), made = 0;
for (int i = 0; i < want; i++)
{
bool got = false; int idx = i;
AddrResourceMgr.Ins.LoadObject<GameObject>(data.s_MonsterPrefab, h =>
{
if (h.Status == AsyncOperationStatus.Succeeded && h.Result != null)
{
var mob = DSUtil.Get_Clone<MobActor>(h.Result);
AddrResourceMgr.Ins.Set_AddressableReleaseSelf(mob.gameObject);
ActorInfo.Ins.Add_Actor(mob, eRole.Mob, cfg.dummySpawnerId, data.s_MonsterPrefab);
mob.Set(true, null, null, cfg.dummySpawnerId, false);
Vector3 pos = DummyPos(pc, off, idx, cfg.dummyGap);
mob.Set(data, pos, null, 0);
if (cfg.dummyStill != 0) mob.Stop(true); // 제자리 더미(쫓아오지 않음) — 0 이면 섬처럼 움직인다
if (idx == 0) { WLTrailTest.Adopt(mob); Dummy = mob; }
var keep = mob.GetComponent<WLTrailTestDummy>();
if (keep == null) mob.gameObject.AddComponent<WLTrailTestDummy>().Begin(mob); // 2마리째부터도 체력 되채움
made++;
}
got = true;
});
while (!got) yield return null;
}
Lap("dummy " + made + "(" + mobId + ")");
}
static Vector3 DummyPos(Actor pc, Vector3 off, int idx, float gap)
{
Vector3 basePos = pc != null ? pc.Get_position() : Vector3.zero;
Quaternion rot = pc != null ? Quaternion.Euler(0f, pc.transform.eulerAngles.y, 0f) : Quaternion.identity;
Vector3 side = rot * Vector3.right;
Vector3 want = basePos + rot * off + side * (gap * ((idx + 1) / 2) * (idx % 2 == 0 ? 1f : -1f)) * (idx == 0 ? 0f : 1f);
UnityEngine.AI.NavMeshHit hit;
if (UnityEngine.AI.NavMesh.SamplePosition(want, out hit, 3f, UnityEngine.AI.NavMesh.AllAreas)) want = hit.position;
return want;
}
/// <summary>더미를 PC 앞 기본 자리로 되돌린다(R 키 · 프로브).</summary>
public static string ResetDummy()
{
var pc = MyValue.MyPC; var d = Dummy;
if (pc == null || d == null) return "PC/더미 없음";
var ms = WLIslandMobSettings.Instance; var cfg = Cfg;
Vector3 off = V3(cfg.dummyOffset); if (off == Vector3.zero && ms != null) off = ms.hitTestDummyOffset;
var pos = DummyPos(pc, off, 0, cfg.dummyGap);
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 list = 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 = list.Count;
list.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + it.itemJson.GroupNo + ")");
}
if (list.Count == 0) return "무기 없음";
int next = curIdx < 0 ? 0 : (curIdx + dir + list.Count) % list.Count;
bool ok = WLInventory.Equip(list[next]);
return (ok ? "장착 " : "실패 ") + names[next] + " " + (next + 1) + "/" + list.Count + (ok ? "" : " · " + WLInventory.LastMessage);
}
/// <summary>보고·프로브용 한 줄.</summary>
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()) + "] pf=[" + string.Join(" · ", PfLog.ToArray()) + "] hangs=[" + string.Join(" ", Hangs.ToArray()) + "] " + LastLog
#if UNITY_EDITOR
+ (string.IsNullOrEmpty(ProfileDump) ? "" : " profile=[" + ProfileDump + "]")
#endif
;
}
// ─────────────────────────────────────────────────────────────────
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);
bool any = h != 0f || v != 0f;
if (any)
{
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();
Vector3 right = Vector3.Cross(Vector3.up, fwd);
pc.Run_byJoystick((fwd * v + right * h).normalized);
}
else if (_moving) { pc.Set_JoystickStatus(0); pc.Set_AutoBraking(true); _moving = false; }
}
if (Input.GetKeyDown(_kPrev)) _weaponMsg = CycleWeapon(-1);
if (Input.GetKeyDown(_kNext)) _weaponMsg = CycleWeapon(+1);
if (Input.GetKeyDown(_kReset)) _weaponMsg = ResetDummy();
}
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 + " 더미 " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") +
"\nWASD 이동 · [ ] 무기 · R 더미 · Q 주력 · E 각성 · 숫자 등급" +
(string.IsNullOrEmpty(_weaponMsg) ? "" : "\n" + _weaponMsg) +
(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);
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d2262201ce81fd5449555b2f0fa8dfed

View File

@ -0,0 +1,304 @@
// ─────────────────────────────────────────────────────────────────────────────
// 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fad1ef88e6d8b98409ef91ae1d984816