#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Reflection; using System.Text; using UnityEditor; using UnityEditor.Animations; using UnityEditor.SceneManagement; using UnityEngine; public static class VerifyExplorerAndCampLocomotion { const string PrefabPath = "Assets/Bundles/Characters/Human/Explorer/Explorer.prefab"; const string ControllerPath = "Assets/Bundles/Characters/Human/Explorer/Explorer.controller"; const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity"; public static string Run() { var sb = new StringBuilder(); sb.AppendLine("=== Automated Verification: Explorer Protagonist & Camp Locomotion ==="); bool allPassed = true; // Phase 1: Explorer Asset & Rigging Validation sb.AppendLine("\n[Phase 1: Explorer Asset & Controller Validation]"); var prefab = AssetDatabase.LoadAssetAtPath(PrefabPath); if (prefab == null) { sb.AppendLine("[FAIL] Explorer.prefab not found at " + PrefabPath); return sb.ToString(); } var animator = prefab.GetComponent(); if (animator == null || animator.avatar == null || !animator.avatar.isValid) { sb.AppendLine("[FAIL] Animator or valid Humanoid Avatar missing on Explorer.prefab"); allPassed = false; } else { sb.AppendLine($"[PASS] Explorer.prefab has valid Humanoid Avatar: {animator.avatar.name}"); } var controller = AssetDatabase.LoadAssetAtPath(ControllerPath); if (controller == null) { sb.AppendLine("[FAIL] Explorer.controller not found at " + ControllerPath); allPassed = false; } else { var sm = controller.layers[0].stateMachine; var states = sm.states.Select(s => s.state.name).ToArray(); bool hasIdle = states.Contains("Idle"); bool hasWalk = states.Contains("Walk"); bool hasParam = controller.parameters.Any(p => p.name == "isMoving"); if (hasIdle && hasWalk && hasParam) { sb.AppendLine($"[PASS] Explorer.controller configured with states: [{string.Join(", ", states)}], default={sm.defaultState.name}, parameter 'isMoving' present"); } else { sb.AppendLine($"[FAIL] Explorer.controller states or parameter missing: hasIdle={hasIdle}, hasWalk={hasWalk}, hasParam={hasParam}"); allPassed = false; } } var smr = prefab.GetComponentInChildren(); if (smr == null || smr.sharedMaterial == null) { sb.AppendLine("[FAIL] SkinnedMeshRenderer or material missing on Explorer.prefab"); allPassed = false; } else { var mat = smr.sharedMaterial; sb.AppendLine($"[PASS] SkinnedMeshRenderer bound to material: {mat.name} (Shader: {mat.shader.name})"); } // Phase 2: Scene Placement & Locomotion Logic Validation sb.AppendLine("\n[Phase 2: Scene Placement & Locomotion Step Simulation]"); var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive); try { var root = scene.GetRootGameObjects().FirstOrDefault(g => g.name == "HomeCamp"); if (root == null) { sb.AppendLine("[FAIL] HomeCamp root not found in " + ScenePath); return sb.ToString(); } var explorerTf = root.transform.Find("Explorer"); if (explorerTf == null) { sb.AppendLine("[FAIL] Explorer not found under HomeCamp scene root"); allPassed = false; return sb.ToString(); } sb.AppendLine($"[PASS] Explorer placed in HomeCamp scene at: {explorerTf.position}, rotation: {explorerTf.eulerAngles}"); // Find Controller Type via reflection var ctrlType = AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType("HotUpdate.Gameplay.CampExplorerController")) .FirstOrDefault(t => t != null); if (ctrlType == null) { sb.AppendLine("[FAIL] Type HotUpdate.Gameplay.CampExplorerController not found in AppDomain"); allPassed = false; return sb.ToString(); } var ctrl = explorerTf.GetComponent(ctrlType); if (ctrl == null) { sb.AppendLine("[FAIL] CampExplorerController missing on Explorer in scene"); allPassed = false; return sb.ToString(); } var isMovingProp = ctrlType.GetProperty("IsMoving"); var targetPosProp = ctrlType.GetProperty("TargetPosition"); var moveToMethod = ctrlType.GetMethod("MoveTo", new[] { typeof(Vector3) }); var stopMethod = ctrlType.GetMethod("Stop"); var stopDistField = ctrlType.GetField("stopDistance"); var moveSpeedField = ctrlType.GetField("moveSpeed"); bool isMoving = (bool)isMovingProp.GetValue(ctrl); if (isMoving) { sb.AppendLine("[FAIL] Explorer initially moving when should be idle"); allPassed = false; } else { sb.AppendLine("[PASS] Initial state: Explorer is idle (IsMoving=False)"); } // Issue Move Command Vector3 startPos = explorerTf.position; Vector3 targetPos = startPos + new Vector3(2.5f, 0, 1.5f); moveToMethod.Invoke(ctrl, new object[] { targetPos }); isMoving = (bool)isMovingProp.GetValue(ctrl); Vector3 curTarget = (Vector3)targetPosProp.GetValue(ctrl); if (!isMoving) { sb.AppendLine("[FAIL] MoveTo did not set IsMoving to true"); allPassed = false; } else { sb.AppendLine($"[PASS] MoveTo({targetPos}) triggered: IsMoving=True, Target={curTarget}"); } // Simulate walk float stopDist = (float)stopDistField.GetValue(ctrl); float moveSpeed = (float)moveSpeedField.GetValue(ctrl); float simDt = 0.05f; int frames = 0; while ((bool)isMovingProp.GetValue(ctrl) && frames < 100) { Vector3 toTarget = curTarget - explorerTf.position; toTarget.y = 0; float dist = toTarget.magnitude; if (dist <= stopDist) { stopMethod.Invoke(ctrl, null); break; } explorerTf.rotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up); explorerTf.position += toTarget.normalized * Mathf.Min(moveSpeed * simDt, dist); frames++; } isMoving = (bool)isMovingProp.GetValue(ctrl); float finalDist = Vector3.Distance(new Vector3(explorerTf.position.x, 0, explorerTf.position.z), new Vector3(curTarget.x, 0, curTarget.z)); if (!isMoving && finalDist <= stopDist + 0.05f) { sb.AppendLine($"[PASS] Explorer successfully walked to target in {frames} steps! Final position: {explorerTf.position}, remaining dist: {finalDist:F3}m, IsMoving={isMoving} (returned to Idle)"); } else { sb.AppendLine($"[FAIL] Explorer failed to reach target: finalDist={finalDist}, IsMoving={isMoving}"); allPassed = false; } // Phase 3: Camera Raycast to Ground sb.AppendLine("\n[Phase 3: Camera Raycast to Ground Verification]"); var camGo = root.transform.Find("HomeCampCamera")?.gameObject; var camera = camGo?.GetComponent(); if (camera == null) { sb.AppendLine("[FAIL] HomeCampCamera missing in scene"); allPassed = false; } else { // Raycast from bottom-center of screen (plaza ground) Ray ray = camera.ViewportPointToRay(new Vector3(0.5f, 0.35f, 0)); int layerMask = 1 << 29; if (Physics.Raycast(ray, out RaycastHit hit, 150f, layerMask)) { sb.AppendLine($"[PASS] Camera raycast hit ground at {hit.point} on collider '{hit.collider.name}' (layer {hit.collider.gameObject.layer})"); } else { sb.AppendLine("[FAIL] Camera raycast missed ground surface"); allPassed = false; } } } finally { EditorSceneManager.CloseScene(scene, true); } sb.AppendLine($"\nOverall Result: {(allPassed ? "ALL TESTS PASSED" : "TESTS FAILED")}"); return sb.ToString(); } } #endif