Files
AIGame/AIClient/AgentScripts/VerifyExplorerAndCampLocomotion.cs

223 lines
9.0 KiB
C#

#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<GameObject>(PrefabPath);
if (prefab == null)
{
sb.AppendLine("[FAIL] Explorer.prefab not found at " + PrefabPath);
return sb.ToString();
}
var animator = prefab.GetComponent<Animator>();
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<AnimatorController>(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<SkinnedMeshRenderer>();
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<Camera>();
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