diff --git a/Assets/Script/Character/Actor.cs b/Assets/Script/Character/Actor.cs index 397934c6f..53b090974 100644 --- a/Assets/Script/Character/Actor.cs +++ b/Assets/Script/Character/Actor.cs @@ -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; diff --git a/Assets/Script/Character/Mob/MobActor.cs b/Assets/Script/Character/Mob/MobActor.cs index 3d5222b86..c75464bb2 100644 --- a/Assets/Script/Character/Mob/MobActor.cs +++ b/Assets/Script/Character/Mob/MobActor.cs @@ -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; diff --git a/Assets/Script/Character/PC/WizardActor.cs b/Assets/Script/Character/PC/WizardActor.cs index 35692e35b..6b9e4ab3d 100644 --- a/Assets/Script/Character/PC/WizardActor.cs +++ b/Assets/Script/Character/PC/WizardActor.cs @@ -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); } diff --git a/Assets/Script/Character/PCActor.cs b/Assets/Script/Character/PCActor.cs index b60f876b9..084bfec52 100644 --- a/Assets/Script/Character/PCActor.cs +++ b/Assets/Script/Character/PCActor.cs @@ -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 diff --git a/Assets/Script/Character/Projectile/ProjectileBase.cs b/Assets/Script/Character/Projectile/ProjectileBase.cs index c206e3689..a97ea95bb 100644 --- a/Assets/Script/Character/Projectile/ProjectileBase.cs +++ b/Assets/Script/Character/Projectile/ProjectileBase.cs @@ -354,7 +354,7 @@ public class ProjectileBase : MonoBehaviour protected List Get_Enemies(Transform tf, float dist) { #if UNITY_EDITOR - if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "StatTest") + if (WL.SkillTest.WLTestScene.Active) return new List { MainToStatTest.Ins.actors[1] }; #endif return ActorInfo.Ins.Get_EnemyActors(m_ProjecTileData.shooter.IsEnemy(), tf.position, dist); diff --git a/Assets/Script/Info/InGameInfo.cs b/Assets/Script/Info/InGameInfo.cs index 26e6db561..884f424b3 100644 --- a/Assets/Script/Info/InGameInfo.cs +++ b/Assets/Script/Info/InGameInfo.cs @@ -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); diff --git a/Assets/Script/Server/ServerInfo.cs b/Assets/Script/Server/ServerInfo.cs index 4dca90450..c084255d2 100644 --- a/Assets/Script/Server/ServerInfo.cs +++ b/Assets/Script/Server/ServerInfo.cs @@ -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) diff --git a/Assets/Script/Util/Play3DSound.cs b/Assets/Script/Util/Play3DSound.cs index c5e1d1873..587e371b3 100644 --- a/Assets/Script/Util/Play3DSound.cs +++ b/Assets/Script/Util/Play3DSound.cs @@ -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) diff --git a/Assets/WL/SkillTest/Editor/WLSkillTestSceneBuilder.cs b/Assets/WL/SkillTest/Editor/WLSkillTestSceneBuilder.cs index 71a7d0e2b..ab10929ec 100644 --- a/Assets/WL/SkillTest/Editor/WLSkillTestSceneBuilder.cs +++ b/Assets/WL/SkillTest/Editor/WLSkillTestSceneBuilder.cs @@ -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()); } - /// 테스트 씬을 연다(Single). 현재 씬이 저장 안 된 상태면 열지 않고 알린다(K-WL-114). 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; } - /// 테스트 씬이 열려 있으면 **저장 없이** 빈 씬으로 바꾼다(다시 만들기 전 · 저장 안 된 변경이 있으면 거절). + /// 테스트 씬이 열려 있으면 저장 없이 빈 씬으로(다시 만들기 전 · 저장 안 된 변경이 있으면 거절). 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(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(); + rt.anchorMin = rt.anchorMax = rt.pivot = anchor; rt.anchoredPosition = pos; rt.sizeDelta = size; + var t = go.AddComponent(); + 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.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(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().sharedMaterial = mat; - GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); // NavMesh 는 부트가 콜라이더 기준 런타임 베이크 + GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); - // 매니저(Title 의 Mgr 과 같은 구성) - var mgr = new GameObject("Mgr"); - mgr.AddComponent(); mgr.AddComponent(); mgr.AddComponent(); - var es = new GameObject("EventSystem"); - es.AddComponent(); es.AddComponent(); + var mgr = new GameObject("Mgr"); mgr.AddComponent(); mgr.AddComponent(); mgr.AddComponent(); + var es = new GameObject("EventSystem"); es.AddComponent(); es.AddComponent(); - // Title 이 Addressables 로 올리던 프리팹 → 씬에 직접 foreach (var n in TitlePrefabs) { var p = AssetDatabase.LoadAssetAtPath(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(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(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(); - new GameObject("DungeonInfo").AddComponent(); - new GameObject("DropItemInfo").AddComponent(); - var gv = new GameObject("Global Volume"); - gv.AddComponent(); - var vol = gv.AddComponent(); - vol.isGlobal = true; vol.enabled = false; // InGame.unity 실측: Volume 컴포넌트 꺼짐 - var profPath = AssetDatabase.GUIDToAssetPath(VolumeProfileGuid); - if (!string.IsNullOrEmpty(profPath)) vol.sharedProfile = AssetDatabase.LoadAssetAtPath(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(PcPrefabPath()); + Actor pcActor = null; + if (pcPrefab != null) { var pc = PrefabUtility.InstantiatePrefab(pcPrefab, scene) as GameObject; pc.transform.SetParent(pcPos.transform, false); pcActor = pc.GetComponent(); } + else sb.Append("🔴 PC 프리팹 없음\n"); + var mobPos = new GameObject("MobPos"); mobPos.transform.position = spawn + new Vector3(2f, 0f, -1.5f); + var mobPrefab = AssetDatabase.LoadAssetAtPath(DummyPrefab); + MobActor mob = null; + if (mobPrefab != null) { var m = PrefabUtility.InstantiatePrefab(mobPrefab, scene) as GameObject; m.transform.SetParent(mobPos.transform, false); mob = m.GetComponent(); } + 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(); - boot.inGameInfoPrefab = AssetDatabase.LoadAssetAtPath(InGameInfoPrefab); - boot.pcInfoPrefab = AssetDatabase.LoadAssetAtPath(PCInfoPrefab); - boot.projectileInfoPrefab = AssetDatabase.LoadAssetAtPath(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.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 50; + canvasGo.AddComponent(); canvasGo.AddComponent(); + 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(); + 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(); diff --git a/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json b/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json index b4c7a137c..614794e4e 100644 --- a/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json +++ b/Assets/WL/SkillTest/Resources/WL/WLSkillTest.json @@ -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] } diff --git a/Assets/WL/SkillTest/WLSkillTestBoot.cs b/Assets/WL/SkillTest/WLSkillTestBoot.cs deleted file mode 100644 index 29b8b5917..000000000 --- a/Assets/WL/SkillTest/WLSkillTestBoot.cs +++ /dev/null @@ -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 Laps = new List(); - /// PlayFab 호출 시각 기록(에디터 · 로딩 실측용 · 2차 Play 에서 로그인 52 s 의 내역을 보려고) - public static readonly List PfLog = new List(); - /// 0.5 s 넘게 멈춘 프레임 기록(시각@길이 · 메인 스레드가 무엇에 막혔는지 PfLog·Laps 와 맞춰 본다) - public static readonly List Hangs = new List(); - 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 - /// 진단: 프로파일러로 잡은 긴 프레임의 가장 무거운 호출 경로(멈춤의 정체) - 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(); - 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(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; } - - /// 상태 파일(Library/ 아래 · 미추적) — 외부 스크립트는 Play 중 `eval` 대신 이 파일을 읽는다. 실측(8차 Play): Play 뒤 첫 `unity command eval/run_script` 가 EditorLoop 에서 약 35 s 메인 스레드를 멈춘다(스크립트 컴파일 준비). - 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("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() == 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(); - if (surf == null) - { - var ground = GameObject.Find("Ground"); - if (ground == null) return; - surf = ground.AddComponent(); - } - 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(data.s_MonsterPrefab, h => - { - if (h.Status == AsyncOperationStatus.Succeeded && h.Result != null) - { - var mob = DSUtil.Get_Clone(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(); - if (keep == null) mob.gameObject.AddComponent().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; - } - - /// 더미를 PC 앞 기본 자리로 되돌린다(R 키 · 프로브). - 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"); - } - - /// 낀 무기를 인벤토리 무기 목록에서 앞뒤로 바꾼다(dir ±1 · 결과 한 줄). - public static string CycleWeapon(int dir) - { - if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드"; - var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); - var list = new List(); var names = new List(); 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); - } - - /// 보고·프로브용 한 줄. - 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); - } - } -} diff --git a/Assets/WL/SkillTest/WLSkillTestBoot.cs.meta b/Assets/WL/SkillTest/WLSkillTestBoot.cs.meta deleted file mode 100644 index 6e69858d8..000000000 --- a/Assets/WL/SkillTest/WLSkillTestBoot.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d2262201ce81fd5449555b2f0fa8dfed \ No newline at end of file diff --git a/Assets/WL/SkillTest/WLSkillTestHarness.cs b/Assets/WL/SkillTest/WLSkillTestHarness.cs new file mode 100644 index 000000000..660747b5a --- /dev/null +++ b/Assets/WL/SkillTest/WLSkillTestHarness.cs @@ -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 +{ + /// 원본 코드의 StatTest 전용 분기가 이 씬에서도 서도록 넓힌 판정(에디터 전용 분기 안에서만 쓰인다). + 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 Laps = new List(); + public static readonly List Hangs = new List(); + 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(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() == null) dummyActor.gameObject.AddComponent().Begin(dummyActor); + Lap("dummy " + mobId); + + // 카메라 — 섬 구도 리그 + 원본 RealCamera 대상 + var icfg = WLIslandSettings.Instance; + if (icfg != null && cfg.cameraRig != 0 && Object.FindFirstObjectByType() == 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(); + if (surf == null) { var ground = GameObject.Find("Ground"); if (ground == null) return; surf = ground.AddComponent(); } + surf.useGeometry = UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders; + surf.BuildNavMesh(); + } + + /// 더미를 PC 앞 기본 자리로(R 키 · 프로브). + 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"); + } + + /// 낀 무기를 인벤토리 무기 목록에서 앞뒤로(dir ±1). + public static string CycleWeapon(int dir) + { + if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드"; + var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); + var ids = new List(); var names = new List(); 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); + } + } +} diff --git a/Assets/WL/SkillTest/WLSkillTestHarness.cs.meta b/Assets/WL/SkillTest/WLSkillTestHarness.cs.meta new file mode 100644 index 000000000..e1b75f1cb --- /dev/null +++ b/Assets/WL/SkillTest/WLSkillTestHarness.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fad1ef88e6d8b98409ef91ae1d984816 \ No newline at end of file