WL #947 스킬 테스트 씬 Play 검증·수정 — 하네스 단계별 try(예외 위치를 상태 파일에) · DungeonInfo 씬 객체 · PC·더미 비활성 저장(Set 직전 켬) · InGameInfo 예열 뒤 PC 생성 · dic_MagicData 채움(HUD 없이 Q/E) · 무기 순환 종류별 1개 + O/P 키(PD 지시) · 평지 가장자리 원점 · 이동 검증 훅(MoveFor) · 빌더 Untitled 에서 Single · 원본 훅 2(InGameInfo: 테스트 씬 DataCheckMgr 제외 · 이펙트 예열 Add 중복 가드)
Play 4회 실측: ready 23.7 s(이펙트 예열 프레임 18 s) · 자동 공격 6 s 10회 · 이동 5.7 m/1.5 s(Run→Idle) · 등급 6 → Q 방패 치기 · E 돌진 베기 발동 · O/P 순환 철창→대검→마법책→대검 · 콘솔 게임 오류 0 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
dc516cc42e
commit
0762f3e2b8
|
|
@ -74,6 +74,9 @@ public partial class InGameInfo : MyCoroutine
|
||||||
|
|
||||||
IEnumerator Start()
|
IEnumerator Start()
|
||||||
{
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (!WL.SkillTest.WLTestScene.Active) // WL #947 훅 — 로그인 없는 테스트 씬은 서버 주기 점검(뉴스 · 일일 초기화 · PlayFab 호출)을 돌리지 않는다
|
||||||
|
#endif
|
||||||
DataCheckMgr.Ins.Set();
|
DataCheckMgr.Ins.Set();
|
||||||
Set_Init();
|
Set_Init();
|
||||||
|
|
||||||
|
|
@ -82,7 +85,7 @@ public partial class InGameInfo : MyCoroutine
|
||||||
var lst = table_effectlist.Ins.Get_DataList(1);
|
var lst = table_effectlist.Ins.Get_DataList(1);
|
||||||
for (int i = 0; i < lst.Count; i++)
|
for (int i = 0; i < lst.Count; i++)
|
||||||
{
|
{
|
||||||
dic_str_Effect.Add(lst[i].s_EffectPrefab, new List<GameObject>());
|
if (!dic_str_Effect.ContainsKey(lst[i].s_EffectPrefab)) dic_str_Effect.Add(lst[i].s_EffectPrefab, new List<GameObject>()); // WL #947 훅 — 테이블을 씬 안에서 로드하는 테스트 씬은 이 루프 전에 예열 투사체 수명 종료(Show_HitEffect → Get_Effect)가 키를 먼저 넣어 Add 가 ArgumentException 으로 끊겼다
|
||||||
for (int j = 0; j < lst[i].n_PreLoadCount; j++)
|
for (int j = 0; j < lst[i].n_PreLoadCount; j++)
|
||||||
Load_Effect(lst[i].s_EffectPrefab, effect => { effect.transform.position = Vector3.one * 100000f; });
|
Load_Effect(lst[i].s_EffectPrefab, effect => { effect.transform.position = Vector3.one * 100000f; });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// WLSkillTestSceneBuilder.cs — 독립 스킬 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장 (PD #947 · 2026-10-20 재지시판)
|
// WLSkillTestSceneBuilder.cs — 독립 스킬 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장 (PD #947 · 2026-10-20 재지시판)
|
||||||
//
|
//
|
||||||
// ■ 들어가는 것(= 원본 StatTest 하네스 구성 + WL 치비·더미·평지) — 로그인·HUD·섬 없음
|
// ■ 들어가는 것(= 원본 StatTest 하네스 구성 + WL 치비·더미·평지) — 로그인·HUD·섬 없음
|
||||||
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 하네스가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem
|
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 하네스가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem · DungeonInfo(InGame.unity 씬 객체 · 웨이브 스탯 표)
|
||||||
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, SortOrder_5(OptionInfo·Popup), ActorInfo}.prefab
|
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, SortOrder_5(OptionInfo·Popup), ActorInfo}.prefab
|
||||||
// · ResWork/Prefabs/Ingame/{InGameInfo(이펙트·투사체 풀), ProjectileInfo, Main Camera(RealCamera)}.prefab
|
// · ResWork/Prefabs/Ingame/{InGameInfo(이펙트·투사체 풀), ProjectileInfo, Main Camera(RealCamera)}.prefab
|
||||||
// · PCPos/치비 PC 프리팹(WLCharacterSwapSettings.pcPrefabName · 없으면 Ai01) · MobPos/더미 몹 프리팹(Res_Addr/Mobs/WL/RPG_Bat)
|
// · PCPos/치비 PC 프리팹(WLCharacterSwapSettings.pcPrefabName · 없으면 Ai01) · MobPos/더미 몹 프리팹(Res_Addr/Mobs/WL/RPG_Bat)
|
||||||
|
|
@ -95,7 +95,10 @@ namespace WL.SkillTest.Editor
|
||||||
if (!AssetDatabase.IsValidFolder(Folder)) AssetDatabase.CreateFolder("Assets/WL", "SkillTest");
|
if (!AssetDatabase.IsValidFolder(Folder)) AssetDatabase.CreateFolder("Assets/WL", "SkillTest");
|
||||||
|
|
||||||
var cfg = WLSkillTestHarness.Cfg;
|
var cfg = WLSkillTestHarness.Cfg;
|
||||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
|
// 현재 씬이 저장 안 된 Untitled(CloseTestScene() 직후)면 Additive 생성이 거절된다(InvalidOperationException · #947 실측) → 그 자리에 Single 로 만든다
|
||||||
|
bool untitled = string.IsNullOrEmpty(prevActive.path);
|
||||||
|
if (untitled && prevActive.isDirty) return "저장 안 된 Untitled 씬이 열려 있음 — Title 을 연 뒤 Build()";
|
||||||
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, untitled ? NewSceneMode.Single : NewSceneMode.Additive);
|
||||||
SceneManager.SetActiveScene(scene);
|
SceneManager.SetActiveScene(scene);
|
||||||
|
|
||||||
var lightGo = new GameObject("Directional Light");
|
var lightGo = new GameObject("Directional Light");
|
||||||
|
|
@ -117,6 +120,8 @@ namespace WL.SkillTest.Editor
|
||||||
|
|
||||||
var mgr = new GameObject("Mgr"); mgr.AddComponent<AddrResourceMgr>(); mgr.AddComponent<CoroutineMgr>(); mgr.AddComponent<TimeMgr>();
|
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 es = new GameObject("EventSystem"); es.AddComponent<EventSystem>(); es.AddComponent<StandaloneInputModule>();
|
||||||
|
// InGame.unity 의 씬 객체(직렬화 참조 0 · 몬스터 웨이브 스탯 표) — 원본 ActorStatInfo.Reset 이 DungeonInfo.Ins.dic_WaveStat 를 읽어 없으면 PC.Set 이 NRE(#947 1차 Play 실측)
|
||||||
|
var dg = new GameObject("DungeonInfo"); dg.AddComponent<DungeonInfo>();
|
||||||
|
|
||||||
foreach (var n in TitlePrefabs)
|
foreach (var n in TitlePrefabs)
|
||||||
{
|
{
|
||||||
|
|
@ -136,12 +141,13 @@ namespace WL.SkillTest.Editor
|
||||||
var pcPos = new GameObject("PCPos"); pcPos.transform.position = spawn;
|
var pcPos = new GameObject("PCPos"); pcPos.transform.position = spawn;
|
||||||
var pcPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PcPrefabPath());
|
var pcPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PcPrefabPath());
|
||||||
Actor pcActor = null;
|
Actor pcActor = null;
|
||||||
if (pcPrefab != null) { var pc = PrefabUtility.InstantiatePrefab(pcPrefab, scene) as GameObject; pc.transform.SetParent(pcPos.transform, false); pcActor = pc.GetComponent<Actor>(); }
|
// 🔴 PC·더미는 꺼 둔 채 저장 — 하네스가 Set 직전에 켠다. 켜진 채 두면 Set(Init_CC) 전에 MobActor.Update→IsCC 가 매 프레임 KeyNotFound(Freeze) 예외(#947 1차 Play 1000건)
|
||||||
|
if (pcPrefab != null) { var pc = PrefabUtility.InstantiatePrefab(pcPrefab, scene) as GameObject; pc.transform.SetParent(pcPos.transform, false); pcActor = pc.GetComponent<Actor>(); pc.SetActive(false); }
|
||||||
else sb.Append("🔴 PC 프리팹 없음\n");
|
else sb.Append("🔴 PC 프리팹 없음\n");
|
||||||
var mobPos = new GameObject("MobPos"); mobPos.transform.position = spawn + new Vector3(2f, 0f, -1.5f);
|
var mobPos = new GameObject("MobPos"); mobPos.transform.position = spawn + new Vector3(2f, 0f, -1.5f);
|
||||||
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(DummyPrefab);
|
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(DummyPrefab);
|
||||||
MobActor mob = null;
|
MobActor mob = null;
|
||||||
if (mobPrefab != null) { var m = PrefabUtility.InstantiatePrefab(mobPrefab, scene) as GameObject; m.transform.SetParent(mobPos.transform, false); mob = m.GetComponent<MobActor>(); }
|
if (mobPrefab != null) { var m = PrefabUtility.InstantiatePrefab(mobPrefab, scene) as GameObject; m.transform.SetParent(mobPos.transform, false); mob = m.GetComponent<MobActor>(); m.SetActive(false); }
|
||||||
else sb.Append("🔴 더미 프리팹 없음 " + DummyPrefab + "\n");
|
else sb.Append("🔴 더미 프리팹 없음 " + DummyPrefab + "\n");
|
||||||
if (pcActor == null) sb.Append("🔴 PC 프리팹에 Actor 없음\n");
|
if (pcActor == null) sb.Append("🔴 PC 프리팹에 Actor 없음\n");
|
||||||
if (mob == null) sb.Append("🔴 더미 프리팹에 MobActor 없음\n");
|
if (mob == null) sb.Append("🔴 더미 프리팹에 MobActor 없음\n");
|
||||||
|
|
@ -164,8 +170,11 @@ namespace WL.SkillTest.Editor
|
||||||
|
|
||||||
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
|
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
|
||||||
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append(" · PC ").Append(PcPrefabPath()).Append('\n');
|
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append(" · PC ").Append(PcPrefabPath()).Append('\n');
|
||||||
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
|
if (!untitled)
|
||||||
EditorSceneManager.CloseScene(scene, true);
|
{ // Title 등에서 불렀으면 원래 씬으로 돌아가고 테스트 씬은 닫는다(Untitled 에서 불렀으면 저장된 테스트 씬이 그대로 열려 있다)
|
||||||
|
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
|
||||||
|
EditorSceneManager.CloseScene(scene, true);
|
||||||
|
}
|
||||||
AssetDatabase.SaveAssets();
|
AssetDatabase.SaveAssets();
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@
|
||||||
"dummyStill": 1,
|
"dummyStill": 1,
|
||||||
"moveKeys": 1,
|
"moveKeys": 1,
|
||||||
"attackKey": "Space",
|
"attackKey": "Space",
|
||||||
"weaponPrevKey": "LeftBracket",
|
"weaponPrevKey": "O",
|
||||||
"weaponNextKey": "RightBracket",
|
"weaponNextKey": "P",
|
||||||
"dummyResetKey": "R",
|
"dummyResetKey": "R",
|
||||||
"slowKey": "T",
|
"slowKey": "T",
|
||||||
"slowScale": 0.2,
|
"slowScale": 0.2,
|
||||||
"pauseKey": "P",
|
"pauseKey": "Backslash",
|
||||||
"label": 1,
|
"label": 1,
|
||||||
"labelFontScale": 0.018,
|
"labelFontScale": 0.018,
|
||||||
"groundSize": 100,
|
"groundSize": 100,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,7 +10,8 @@
|
||||||
// ■ 원본 코드와의 접점 = StatTest 전용 분기 13곳을 `WLTestScene.Active`(StatTest 또는 WLSkillTest)로 넓힌 것뿐(HUD·포털·서버 일시정지·대상 목록 분기 · 전부 #if UNITY_EDITOR 안).
|
// ■ 원본 코드와의 접점 = StatTest 전용 분기 13곳을 `WLTestScene.Active`(StatTest 또는 WLSkillTest)로 넓힌 것뿐(HUD·포털·서버 일시정지·대상 목록 분기 · 전부 #if UNITY_EDITOR 안).
|
||||||
// 이동·넉백·소환 AI 분기(Actor 1665·1710·2198 · MobActor 75 · SummonActor 60 · WizardActor 53·61)는 StatTest 전용으로 남겨 **실제 게임 동작**을 유지한다.
|
// 이동·넉백·소환 AI 분기(Actor 1665·1710·2198 · MobActor 75 · SummonActor 60 · WizardActor 53·61)는 StatTest 전용으로 남겨 **실제 게임 동작**을 유지한다.
|
||||||
// ■ 이 클래스가 MainToStatTest 를 상속하는 이유 = 원본 분기가 `MainToStatTest.Ins.actors[1]`(대상)·`Set_Dmglabel`(피해 글자)를 부른다 → 같은 자리를 채워 준다.
|
// ■ 이 클래스가 MainToStatTest 를 상속하는 이유 = 원본 분기가 `MainToStatTest.Ins.actors[1]`(대상)·`Set_Dmglabel`(피해 글자)를 부른다 → 같은 자리를 채워 준다.
|
||||||
// ■ 키: WASD/방향키 이동 · Space 기본 공격 1타 · Q 주력 · E 각성 · 숫자 등급(기존 훅) · [ ] 무기 바꾸기 · R 더미 제자리 · T 슬로 모션 · P 일시정지
|
// ■ 키: WASD/방향키 이동(섬 조이스틱과 같은 호출 · Set_AutoBraking(false) → JoystickStatus 1 → Run_byJoystick) · Space 기본 공격 1타 · Q 주력 · E 각성 · 숫자 등급(기존 훅)
|
||||||
|
// · O 무기 이전(왼쪽 순환) · P 무기 다음(오른쪽 순환) — PD 2026-10-20 #947 지시 · 종류(GroupNo)마다 1개 · R 더미 제자리 · T 슬로 모션 · Pause = 설정값(기본 Backslash)
|
||||||
// ■ 값 = Resources/WL/WLSkillTest.json (C45) · 상태 파일 = Library/WLSkillTest_status.txt(외부 스크립트는 Play 중 eval 대신 이 파일) · 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업)
|
// ■ 값 = Resources/WL/WLSkillTest.json (C45) · 상태 파일 = Library/WLSkillTest_status.txt(외부 스크립트는 Play 중 eval 대신 이 파일) · 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
@ -45,12 +46,12 @@ namespace WL.SkillTest
|
||||||
public int dummyStill = 1; // 1 = 더미 제자리
|
public int dummyStill = 1; // 1 = 더미 제자리
|
||||||
public int moveKeys = 1;
|
public int moveKeys = 1;
|
||||||
public string attackKey = "Space";
|
public string attackKey = "Space";
|
||||||
public string weaponPrevKey = "LeftBracket";
|
public string weaponPrevKey = "O"; // PD #947: O = 무기 왼쪽 순환
|
||||||
public string weaponNextKey = "RightBracket";
|
public string weaponNextKey = "P"; // PD #947: P = 무기 오른쪽 순환
|
||||||
public string dummyResetKey = "R";
|
public string dummyResetKey = "R";
|
||||||
public string slowKey = "T";
|
public string slowKey = "T";
|
||||||
public float slowScale = 0.2f;
|
public float slowScale = 0.2f;
|
||||||
public string pauseKey = "P";
|
public string pauseKey = "Backslash"; // P 를 무기에 내주고 일시정지는 \ 로
|
||||||
public int label = 1;
|
public int label = 1;
|
||||||
public float labelFontScale = 0.018f;
|
public float labelFontScale = 0.018f;
|
||||||
public float groundSize = 100f;
|
public float groundSize = 100f;
|
||||||
|
|
@ -72,11 +73,13 @@ namespace WL.SkillTest
|
||||||
public static readonly List<string> Laps = new List<string>();
|
public static readonly List<string> Laps = new List<string>();
|
||||||
public static readonly List<string> Hangs = new List<string>();
|
public static readonly List<string> Hangs = new List<string>();
|
||||||
public static float T0;
|
public static float T0;
|
||||||
|
public static string MoveLog = "";
|
||||||
|
|
||||||
static WLSkillTestJson s_cfg;
|
static WLSkillTestJson s_cfg;
|
||||||
float _lastRt;
|
static System.Reflection.FieldInfo s_fEffects, s_fMagic, s_fMagicArr;
|
||||||
|
float _lastRt, _simH, _simV;
|
||||||
bool _moving, _slow;
|
bool _moving, _slow;
|
||||||
KeyCode _kAttack = KeyCode.Space, _kPrev = KeyCode.LeftBracket, _kNext = KeyCode.RightBracket, _kReset = KeyCode.R, _kSlow = KeyCode.T, _kPause = KeyCode.P;
|
KeyCode _kAttack = KeyCode.Space, _kPrev = KeyCode.O, _kNext = KeyCode.P, _kReset = KeyCode.R, _kSlow = KeyCode.T, _kPause = KeyCode.Backslash;
|
||||||
string _msg = "";
|
string _msg = "";
|
||||||
GUIStyle _style;
|
GUIStyle _style;
|
||||||
|
|
||||||
|
|
@ -106,60 +109,139 @@ namespace WL.SkillTest
|
||||||
T0 = Time.realtimeSinceStartup;
|
T0 = Time.realtimeSinceStartup;
|
||||||
Laps.Clear(); Hangs.Clear(); Ready = false; Dummy = null; s_cfg = null; LastLog = "";
|
Laps.Clear(); Hangs.Clear(); Ready = false; Dummy = null; s_cfg = null; LastLog = "";
|
||||||
var cfg = Cfg;
|
var cfg = Cfg;
|
||||||
_kAttack = Key(cfg.attackKey, KeyCode.Space); _kPrev = Key(cfg.weaponPrevKey, KeyCode.LeftBracket); _kNext = Key(cfg.weaponNextKey, KeyCode.RightBracket);
|
_kAttack = Key(cfg.attackKey, KeyCode.Space); _kPrev = Key(cfg.weaponPrevKey, KeyCode.O); _kNext = Key(cfg.weaponNextKey, KeyCode.P);
|
||||||
_kReset = Key(cfg.dummyResetKey, KeyCode.R); _kSlow = Key(cfg.slowKey, KeyCode.T); _kPause = Key(cfg.pauseKey, KeyCode.P);
|
_kReset = Key(cfg.dummyResetKey, KeyCode.R); _kSlow = Key(cfg.slowKey, KeyCode.T); _kPause = Key(cfg.pauseKey, KeyCode.Backslash);
|
||||||
|
MoveLog = ""; _simH = _simV = 0f;
|
||||||
if (actors == null || actors.Length < 2) actors = new Actor[] { pcActor, dummyActor };
|
if (actors == null || actors.Length < 2) actors = new Actor[] { pcActor, dummyActor };
|
||||||
Lap("awake");
|
Lap("awake");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 단계마다 try — 예외는 상태 파일(stage=<단계>-fail · 예외 종류 · 메시지 · 호출 위치 4줄)에 남긴다. 코루틴이 조용히 죽으면 콘솔 스팸에 묻혀 찾기 어려웠다(#947 1차 Play · PC.Set NRE 가 Freeze 1000건 아래)
|
||||||
IEnumerator Start()
|
IEnumerator Start()
|
||||||
{
|
{
|
||||||
var cfg = Cfg;
|
var cfg = Cfg;
|
||||||
BakeGround(); ApplySky(cfg);
|
if (!Try("navmesh", () => { BakeGround(); ApplySky(cfg); })) yield break;
|
||||||
Lap("navmesh");
|
Lap("navmesh");
|
||||||
|
|
||||||
while (TableChecker.Ins == null || !TableChecker.Ins.CheckAllLoad()) yield return null;
|
while (TableChecker.Ins == null || !TableChecker.Ins.CheckAllLoad()) yield return null;
|
||||||
Lap("tables");
|
Lap("tables");
|
||||||
|
|
||||||
if (pcActor == null || dummyActor == null) { Stage = "refs-missing"; LastLog = "pcActor/dummyActor 참조 없음 — Build Scene 다시"; WriteStatus(); yield break; }
|
// InGameInfo.Start 의 이펙트 예열 루프(테이블 0.1 s 폴링 뒤 1프레임)가 끝난 다음에 PC 를 만든다 — 먼저 만들면 PC 피격 이펙트 키가 먼저 등록돼 그 루프의 Add 가 ArgumentException 으로 끊긴다(#947 2차 Play 실측)
|
||||||
|
for (float t = 0f; t < 3f && !EffectsPreloaded(); t += Time.unscaledDeltaTime) yield return null;
|
||||||
|
Lap("ingameinfo");
|
||||||
|
|
||||||
// 오프라인 서버 데이터(신규 유저 꼴 · 테이블 로딩 뒤에만 생성 가능) + 자동 전투
|
if (pcActor == null || dummyActor == null) { Fail("refs-missing", "pcActor/dummyActor 참조 없음 — Build Scene 다시"); 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 무기 적용기·스타터 킷이 돈다(섬과 같은 경로)
|
// PC — 직업 = 낀 WL 무기(없으면 baseClassId). 원본 Set 안에서 WL 무기 적용기·스타터 킷이 돈다(섬과 같은 경로)
|
||||||
int cls = ResolveClass(cfg);
|
int cls = 0;
|
||||||
pcActor.transform.position = NavPos(V3(cfg.spawnPos));
|
if (!Try("pc", () =>
|
||||||
pcActor.Set(false, null, sdata, cls, true);
|
{
|
||||||
ActorInfo.Ins.Add_Actor(pcActor, eRole.PC, 0, pcActor.name);
|
// 오프라인 서버 데이터(신규 유저 꼴 · 테이블 로딩 뒤에만 생성 가능) + 자동 전투
|
||||||
|
var sdata = new ServerData();
|
||||||
|
try { if (cfg.autoBattle != 0) sdata.Add_Common(eCommon.Auto, 1); } catch (System.Exception ex) { LastLog = "Auto 설정 실패 " + ex.Message; }
|
||||||
|
cls = ResolveClass(cfg);
|
||||||
|
pcActor.transform.position = NavPos(V3(cfg.spawnPos));
|
||||||
|
pcActor.gameObject.SetActive(true); // 빌더가 꺼 둔 것을 Set 직전에 켠다(원본 Set 도 켜지만 그 전에 콜라이더 bounds 를 읽는다)
|
||||||
|
pcActor.Set(false, null, sdata, cls, true);
|
||||||
|
FillMagicSlots(pcActor, sdata);
|
||||||
|
ActorInfo.Ins.Add_Actor(pcActor, eRole.PC, 0, pcActor.name);
|
||||||
|
})) yield break;
|
||||||
Lap("pc " + cls);
|
Lap("pc " + cls);
|
||||||
|
|
||||||
// 더미 — 몬스터 표 행으로 능력치 · 무적(WLTrailTest 체력 되채움) · 제자리
|
// 더미 — 몬스터 표 행으로 능력치 · 무적(WLTrailTest 체력 되채움) · 제자리
|
||||||
var ms = WLIslandMobSettings.Instance;
|
int mobId = 0;
|
||||||
int mobId = cfg.dummyMobId > 0 ? cfg.dummyMobId : (ms != null ? ms.hitTestDummyMobId : 101);
|
if (!Try("dummy", () =>
|
||||||
var data = table_monsterlist.Ins.Get_Data_orNull(mobId);
|
{
|
||||||
if (data == null) { Stage = "mob-row-none"; LastLog = "몬스터 표 없음 " + mobId; WriteStatus(); yield break; }
|
var ms = WLIslandMobSettings.Instance;
|
||||||
ActorInfo.Ins.Add_Actor(dummyActor, eRole.Mob, 1, dummyActor.name);
|
mobId = cfg.dummyMobId > 0 ? cfg.dummyMobId : (ms != null ? ms.hitTestDummyMobId : 101);
|
||||||
dummyActor.Set(true, null, null, 1, false);
|
var data = table_monsterlist.Ins.Get_Data_orNull(mobId);
|
||||||
dummyActor.Set(data, DummyPos(cfg, ms), null, 0);
|
if (data == null) throw new System.Exception("몬스터 표 없음 " + mobId);
|
||||||
if (cfg.dummyStill != 0) dummyActor.Stop(true);
|
dummyActor.gameObject.SetActive(true); // 빌더가 꺼 둔 것(Set 전 Update 의 IsCC 예외 방지) — Set 이 자식 SkinnedMeshRenderer bounds 를 읽으므로 먼저 켠다
|
||||||
WLTrailTest.Adopt(dummyActor); Dummy = dummyActor;
|
ActorInfo.Ins.Add_Actor(dummyActor, eRole.Mob, 1, dummyActor.name);
|
||||||
if (dummyActor.GetComponent<WLTrailTestDummy>() == null) dummyActor.gameObject.AddComponent<WLTrailTestDummy>().Begin(dummyActor);
|
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);
|
||||||
|
})) yield break;
|
||||||
Lap("dummy " + mobId);
|
Lap("dummy " + mobId);
|
||||||
|
|
||||||
// 카메라 — 섬 구도 리그 + 원본 RealCamera 대상
|
// 카메라 — 섬 구도 리그 + 원본 RealCamera 대상
|
||||||
var icfg = WLIslandSettings.Instance;
|
if (!Try("camera", () =>
|
||||||
if (icfg != null && cfg.cameraRig != 0 && Object.FindFirstObjectByType<WLDungeonCameraRig>() == null) WLDungeonCameraRig.Create(icfg, gameObject.scene);
|
{
|
||||||
var rc = MyValue.m_RealCamera;
|
var icfg = WLIslandSettings.Instance;
|
||||||
if (rc != null) rc.Set_Target(pcActor.transform, icfg != null ? WLDungeonCameraRig.SolvedYaw(icfg) : 0f);
|
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 break;
|
||||||
yield return null;
|
yield return null;
|
||||||
|
|
||||||
pcActor.Stop(false);
|
if (!Try("ready", () => { pcActor.Stop(false); pcActor.Change_Target(false, dummyActor); })) yield break;
|
||||||
pcActor.Change_Target(false, dummyActor);
|
|
||||||
Ready = true;
|
Ready = true;
|
||||||
Lap("ready");
|
Lap("ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Try(string stage, System.Action step)
|
||||||
|
{
|
||||||
|
try { step(); return true; }
|
||||||
|
catch (System.Exception ex) { Fail(stage + "-fail", ex.GetType().Name + ": " + ex.Message + " @ " + Where(ex)); return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>예외 호출 위치 4줄 — 인자 목록·IL 오프셋·절대 경로를 지워 「at 클래스.메서드 파일.cs:줄」 만 남긴다.</summary>
|
||||||
|
static string Where(System.Exception ex)
|
||||||
|
{
|
||||||
|
var lines = (ex.StackTrace ?? "").Split('\n'); var sb = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < lines.Length && i < 4; i++)
|
||||||
|
sb.Append(System.Text.RegularExpressions.Regex.Replace(lines[i].Trim(), @"\s*\(.*?\)\s*\[0x[0-9a-fA-F]+\]\s*in\s*.*[\\/]", " ")).Append(" | ");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fail(string stage, string msg) { Stage = stage; LastLog = msg; WriteStatus(); Debug.LogWarning("[WLSkillTest] " + stage + " — " + msg); } // LogError 금지(원본 ErrorLogHook 팝업)
|
||||||
|
|
||||||
|
/// <summary>InGameInfo.Start 의 예열 루프가 돌았는가 — 비공개 dic_str_Effect 의 키 수 ≥ 이펙트 표(목록 1) 행 수.</summary>
|
||||||
|
static bool EffectsPreloaded()
|
||||||
|
{
|
||||||
|
var ig = InGameInfo.Ins; if (ig == null) return false;
|
||||||
|
if (s_fEffects == null) s_fEffects = typeof(InGameInfo).GetField("dic_str_Effect", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
|
||||||
|
var d = s_fEffects != null ? s_fEffects.GetValue(ig) as System.Collections.IDictionary : null;
|
||||||
|
if (d == null) return true; // 필드가 바뀌었으면 기다리지 않는다
|
||||||
|
var lst = table_effectlist.Ins != null ? table_effectlist.Ins.Get_DataList(1) : null;
|
||||||
|
return lst == null || d.Count >= lst.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>원본 PCActor.Set 은 HUD 스킬 카드(NewGameUI)가 있을 때만 dic_MagicData 를 채운다 → HUD 없는 이 씬에선 Use_Skill 의 dic_MagicData[index] 가 KeyNotFound(#947 2차 Play · Q/E 발동 실패). 같은 꼴로 스킬 칸 수만큼 채운다.</summary>
|
||||||
|
static void FillMagicSlots(Actor pc, ServerData sdata)
|
||||||
|
{
|
||||||
|
var bf = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public;
|
||||||
|
if (s_fMagic == null) s_fMagic = typeof(Actor).GetField("dic_MagicData", bf);
|
||||||
|
if (s_fMagicArr == null) s_fMagicArr = typeof(PCActor).GetField("arr_magicData", bf);
|
||||||
|
var dic = s_fMagic != null ? s_fMagic.GetValue(pc) as Dictionary<int, UseMagicInfo> : null;
|
||||||
|
if (dic == null) { LastLog = "dic_MagicData 없음"; return; }
|
||||||
|
var arr = s_fMagicArr != null ? s_fMagicArr.GetValue(pc) as System.Array : null;
|
||||||
|
int n = arr != null ? arr.Length : 6;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
if (!dic.ContainsKey(i)) dic.Add(i, new UseMagicInfo { m_uiIndex = i, m_SkillTableData = table_skilllist.Ins.Get_Data_orNull(sdata.Get_EquipSkillID(i)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>검증용 — 이동 키를 seconds 동안 누른 것과 같은 호출(카메라 기준 h · v · Update 의 같은 경로)을 돌리고 이동 거리를 MoveLog 에 남긴다.</summary>
|
||||||
|
public static string MoveFor(float seconds, float h, float v)
|
||||||
|
{
|
||||||
|
var ins = Ins as WLSkillTestHarness; if (ins == null) return "하네스 없음";
|
||||||
|
if (!Ready || MyValue.MyPC == null) return "ready 아님";
|
||||||
|
MoveLog = "이동 중"; ins.StartCoroutine(ins.CoMove(seconds, h, v));
|
||||||
|
return "이동 시작 " + seconds + " s (h " + h + " · v " + v + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator CoMove(float seconds, float h, float v)
|
||||||
|
{
|
||||||
|
var pc = MyValue.MyPC as PCActor; if (pc == null) yield break;
|
||||||
|
Vector3 p0 = pc.Get_position(); float t0 = Time.time; string animMid = "-";
|
||||||
|
while (Time.time - t0 < seconds) { _simH = h; _simV = v; animMid = pc.Get_CurAnim().ToString(); yield return null; }
|
||||||
|
_simH = _simV = 0f;
|
||||||
|
yield return null;
|
||||||
|
MoveLog = "이동 " + Vector3.Distance(p0, pc.Get_position()).ToString("F2") + " m / " + seconds + " s · 이동 중 anim " + animMid + " → 뗀 뒤 " + pc.Get_CurAnim() + " · joystick " + pc.JoystickStatus;
|
||||||
|
}
|
||||||
|
|
||||||
void OnDestroy() { try { if (System.IO.File.Exists(StatusPath)) System.IO.File.Delete(StatusPath); } catch { } Time.timeScale = 1f; }
|
void OnDestroy() { try { if (System.IO.File.Exists(StatusPath)) System.IO.File.Delete(StatusPath); } catch { } Time.timeScale = 1f; }
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -216,22 +298,27 @@ namespace WL.SkillTest
|
||||||
return "더미 " + pos.ToString("F1");
|
return "더미 " + pos.ToString("F1");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>낀 무기를 인벤토리 무기 목록에서 앞뒤로(dir ±1).</summary>
|
/// <summary>낀 무기를 무기 종류(GroupNo) 순으로 앞뒤로(dir ±1 · O = -1 · P = +1). 종류마다 1개 — SortedItems 는 장착 중인 것을 맨 앞에 두므로 그대로 쓰면 같은 종류 2개 사이를 왕복했다(#947 2차 Play 실측 · 묵광 2개).</summary>
|
||||||
public static string CycleWeapon(int dir)
|
public static string CycleWeapon(int dir)
|
||||||
{
|
{
|
||||||
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
|
if (!WLInventory.EnsureLoaded()) return "인벤토리 미로드";
|
||||||
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon);
|
||||||
var ids = new List<int>(); var names = new List<string>(); int curIdx = -1;
|
int curGroup = cur != null && cur.itemJson != null ? cur.itemJson.GroupNo : -1;
|
||||||
|
var grp = new List<int>(); var ids = new List<int>(); var names = new List<string>();
|
||||||
foreach (var it in WLInventory.SortedItems())
|
foreach (var it in WLInventory.SortedItems())
|
||||||
{
|
{
|
||||||
if (it == null || it.itemJson == null || !it.itemJson.IsEquipment || it.itemJson.EquipmentType != eFREquipmentType.Weapon) continue;
|
if (it == null || it.itemJson == null || !it.itemJson.IsEquipment || it.itemJson.EquipmentType != eFREquipmentType.Weapon) continue;
|
||||||
if (cur != null && ReferenceEquals(it, cur)) curIdx = ids.Count;
|
int g = it.itemJson.GroupNo;
|
||||||
ids.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + it.itemJson.GroupNo + ")");
|
if (grp.IndexOf(g) >= 0) continue; // 같은 종류는 첫 것(장착 중이면 그것)만
|
||||||
|
grp.Add(g); ids.Add(it.rawItem._id); names.Add(it.itemJson.Item_name + "(" + g + ")");
|
||||||
}
|
}
|
||||||
if (ids.Count == 0) return "무기 없음";
|
if (grp.Count == 0) return "무기 없음";
|
||||||
int next = curIdx < 0 ? 0 : (curIdx + dir + ids.Count) % ids.Count;
|
var order = new List<int>(); for (int i = 0; i < grp.Count; i++) order.Add(i);
|
||||||
|
order.Sort((a, b) => grp[a].CompareTo(grp[b]));
|
||||||
|
int curIdx = -1; for (int i = 0; i < order.Count; i++) if (grp[order[i]] == curGroup) { curIdx = i; break; }
|
||||||
|
int pos = curIdx < 0 ? 0 : (curIdx + dir + order.Count) % order.Count, next = order[pos];
|
||||||
bool ok = WLInventory.Equip(ids[next]);
|
bool ok = WLInventory.Equip(ids[next]);
|
||||||
return (ok ? "장착 " : "실패 ") + names[next] + " " + (next + 1) + "/" + ids.Count + (ok ? "" : " · " + WLInventory.LastMessage);
|
return (ok ? "장착 " : "실패 ") + names[next] + " " + (pos + 1) + "/" + order.Count + (ok ? "" : " · " + WLInventory.LastMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string Describe()
|
public static string Describe()
|
||||||
|
|
@ -258,21 +345,28 @@ namespace WL.SkillTest
|
||||||
if (pc == null) return;
|
if (pc == null) return;
|
||||||
var cfg = Cfg;
|
var cfg = Cfg;
|
||||||
|
|
||||||
|
// 이동 — 섬 조이스틱(WLIslandAvatar.DrivePc · 원본 FloatingJoystick)과 같은 호출 순서. 누름 = Set_AutoBraking(false) → JoystickStatus 1 · 매 프레임 Run_byJoystick(카메라 기준) · 뗌 = JoystickStatus 0 → Set_AutoBraking(true)(Play_Idle · 자동 전투 재개)
|
||||||
|
float h = 0f, v = 0f;
|
||||||
if (cfg.moveKeys != 0)
|
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);
|
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);
|
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 (_simH != 0f || _simV != 0f) { h = _simH; v = _simV; } // 검증 프로브(MoveFor) — 키와 같은 경로
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// 평지 가장자리 — 공격 전진·피격 넉백으로 PC·더미가 1분에 약 20 m 흘러간다(#947 2차 Play 실측) → 끝에 닿기 전에 둘 다 원점으로
|
||||||
|
float half = cfg.groundSize * 0.5f - 3f; var pp = pc.Get_position();
|
||||||
|
if (Mathf.Abs(pp.x) > half || Mathf.Abs(pp.z) > half) { pc.Set_Warp(NavPos(V3(cfg.spawnPos))); _msg = "평지 가장자리 — PC·더미 원점 " + ResetDummy(); }
|
||||||
|
|
||||||
if (Input.GetKeyDown(_kAttack)) { if (Dummy != null) pc.Change_Target(false, Dummy); pc.Play_Attack(0, 1f); _msg = "기본 공격"; }
|
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(_kPrev)) _msg = CycleWeapon(-1);
|
||||||
|
|
@ -294,7 +388,8 @@ namespace WL.SkillTest
|
||||||
if (WLInventory.Loaded) { var cur = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (cur != null && cur.itemJson != null) w = cur.itemJson.Item_name + "(" + cur.itemJson.GroupNo + ")"; }
|
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 ? "" : " …") +
|
string s = "WL 스킬 테스트 · " + Stage + (Ready ? "" : " …") +
|
||||||
"\n무기 " + w + " 더미 HP " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") + (_slow ? " [슬로 ×" + Time.timeScale + "]" : "") +
|
"\n무기 " + w + " 더미 HP " + (Dummy != null ? Dummy.Get_HP().ToString("F0") : "-") + (_slow ? " [슬로 ×" + Time.timeScale + "]" : "") +
|
||||||
"\nWASD 이동 · Space 공격 · Q 주력 · E 각성 · 숫자 등급 · [ ] 무기 · R 더미 · T 슬로 · P 일시정지" +
|
"\nWASD 이동 · " + _kAttack + " 공격 · " + WLSkillHotkey.Key + " 주력 · " + (WLSkillHotkey.Key2 != KeyCode.None ? WLSkillHotkey.Key2 + " 각성 · " : "") + "숫자 등급 · " +
|
||||||
|
_kPrev + " 무기 이전 · " + _kNext + " 무기 다음 · " + _kReset + " 더미 · " + _kSlow + " 슬로 · " + _kPause + " 일시정지" +
|
||||||
(string.IsNullOrEmpty(_msg) ? "" : "\n" + _msg) + (Ready ? "\n" + string.Join(" · ", Laps.ToArray()) : "");
|
(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);
|
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 = new Color(0f, 0f, 0f, 0.6f); GUI.Label(new Rect(r.x + 1, r.y + 1, r.width, r.height), s, _style);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue