TeamPerceptionVisualizer.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. /// <summary>
  4. /// Visualization component that displays a semi-transparent purple circle around the team marker
  5. /// to represent the team's collective perception range. The radius is based on the highest
  6. /// perception value among team members.
  7. /// </summary>
  8. public class TeamPerceptionVisualizer : MonoBehaviour
  9. {
  10. [Header("Perception Circle Settings")]
  11. [SerializeField] private Material perceptionCircleMaterial;
  12. [SerializeField] private Color perceptionColor = new Color(0.5f, 0f, 1f, 0.5f); // 50% transparent purple
  13. [SerializeField] private float perceptionMultiplier = 1.0f; // Converts perception value to world units
  14. [SerializeField] private int circleSegments = 64; // Circle resolution
  15. [SerializeField] private float heightOffset = 0.1f; // How high above ground to place the circle
  16. [Header("Dynamic Updates")]
  17. [SerializeField] private bool updatePerceptionInRealTime = true;
  18. [SerializeField] private float updateInterval = 1f; // How often to check for perception changes (in seconds)
  19. [SerializeField] private bool isVisualizationEnabled = true; // Can be controlled by UI
  20. [Header("Debug")]
  21. [SerializeField] private bool showDebugInfo = false;
  22. // Private variables
  23. private GameObject perceptionCircleObject;
  24. private LineRenderer circleRenderer;
  25. private SimpleTeamPlacement teamPlacement;
  26. private GameStateManager gameStateManager;
  27. private MainTeamSelectScript teamSelectScript;
  28. private int lastKnownMaxPerception = -1;
  29. private float lastUpdateTime = 0f;
  30. public static TeamPerceptionVisualizer Instance { get; private set; }
  31. void Awake()
  32. {
  33. if (Instance == null)
  34. {
  35. Instance = this;
  36. }
  37. else
  38. {
  39. Debug.LogWarning("Multiple TeamPerceptionVisualizer instances found. Destroying this one.");
  40. Destroy(gameObject);
  41. return;
  42. }
  43. }
  44. void Start()
  45. {
  46. // Find required components
  47. teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
  48. gameStateManager = FindFirstObjectByType<GameStateManager>();
  49. teamSelectScript = FindFirstObjectByType<MainTeamSelectScript>();
  50. if (teamPlacement == null)
  51. {
  52. Debug.LogError("TeamPerceptionVisualizer: SimpleTeamPlacement component not found!");
  53. return;
  54. }
  55. // Create the perception circle
  56. CreatePerceptionCircle();
  57. // Load visualization state from PlayerPrefs
  58. isVisualizationEnabled = PlayerPrefs.GetInt("ShowPerceptionVisualization", 1) == 1;
  59. // Initial position update with slight delay to ensure team marker is loaded
  60. StartCoroutine(DelayedInitialUpdate());
  61. }
  62. /// <summary>
  63. /// Performs initial updates with a small delay to ensure all components are loaded
  64. /// </summary>
  65. private System.Collections.IEnumerator DelayedInitialUpdate()
  66. {
  67. yield return new WaitForEndOfFrame();
  68. yield return new WaitForSeconds(0.1f); // Small delay
  69. UpdateCirclePosition();
  70. UpdatePerceptionVisualization();
  71. }
  72. void Update()
  73. {
  74. // Early exit if visualization is disabled
  75. if (!isVisualizationEnabled)
  76. {
  77. // Hide the circle if it's currently visible
  78. if (perceptionCircleObject != null && perceptionCircleObject.activeSelf)
  79. {
  80. perceptionCircleObject.SetActive(false);
  81. }
  82. return;
  83. }
  84. // Always update position to follow team marker
  85. UpdateCirclePosition();
  86. if (!updatePerceptionInRealTime || Time.time - lastUpdateTime < updateInterval)
  87. return;
  88. UpdatePerceptionVisualization();
  89. lastUpdateTime = Time.time;
  90. }
  91. /// <summary>
  92. /// Creates the visual circle GameObject and LineRenderer component
  93. /// </summary>
  94. private void CreatePerceptionCircle()
  95. {
  96. // Create the circle GameObject
  97. perceptionCircleObject = new GameObject("PerceptionCircle");
  98. perceptionCircleObject.transform.SetParent(this.transform);
  99. // Add and configure LineRenderer
  100. circleRenderer = perceptionCircleObject.AddComponent<LineRenderer>();
  101. circleRenderer.useWorldSpace = false;
  102. circleRenderer.loop = true;
  103. circleRenderer.startWidth = 0.3f;
  104. circleRenderer.endWidth = 0.3f;
  105. circleRenderer.positionCount = circleSegments;
  106. circleRenderer.startColor = perceptionColor;
  107. circleRenderer.endColor = perceptionColor;
  108. // Set material if provided, otherwise create a simple one
  109. if (perceptionCircleMaterial != null)
  110. {
  111. circleRenderer.material = perceptionCircleMaterial;
  112. }
  113. else
  114. {
  115. // Create a simple material with transparency support
  116. Material simpleMaterial = new Material(Shader.Find("Sprites/Default"));
  117. simpleMaterial.color = perceptionColor;
  118. circleRenderer.material = simpleMaterial;
  119. }
  120. // Initially hide the circle until we have valid data
  121. perceptionCircleObject.SetActive(false);
  122. }
  123. /// <summary>
  124. /// Updates the circle position to follow the team marker
  125. /// </summary>
  126. private void UpdateCirclePosition()
  127. {
  128. if (perceptionCircleObject == null || teamPlacement == null)
  129. return;
  130. Vector3 teamPosition = GetTeamMarkerPosition();
  131. if (teamPosition != Vector3.zero)
  132. {
  133. Vector3 newPosition = new Vector3(
  134. teamPosition.x,
  135. teamPosition.y + heightOffset,
  136. teamPosition.z
  137. );
  138. // Only move if the position actually changed (to avoid spam)
  139. if (Vector3.Distance(perceptionCircleObject.transform.position, newPosition) > 0.1f)
  140. {
  141. perceptionCircleObject.transform.position = newPosition;
  142. if (showDebugInfo)
  143. {
  144. Debug.Log($"TeamPerceptionVisualizer: Updated circle position to {newPosition}");
  145. }
  146. }
  147. }
  148. else if (showDebugInfo)
  149. {
  150. Debug.LogWarning("TeamPerceptionVisualizer: Could not find team marker position");
  151. }
  152. }
  153. /// <summary>
  154. /// Updates the perception visualization based on current team data
  155. /// </summary>
  156. public void UpdatePerceptionVisualization()
  157. {
  158. if (circleRenderer == null)
  159. return;
  160. int maxPerception = GetHighestTeamPerception();
  161. // If no valid perception data, hide the circle
  162. if (maxPerception <= 0)
  163. {
  164. if (perceptionCircleObject.activeSelf)
  165. {
  166. perceptionCircleObject.SetActive(false);
  167. if (showDebugInfo)
  168. Debug.Log("TeamPerceptionVisualizer: Hiding perception circle - no valid team data");
  169. }
  170. return;
  171. }
  172. // If perception hasn't changed, no need to update
  173. if (maxPerception == lastKnownMaxPerception && perceptionCircleObject.activeSelf)
  174. return;
  175. lastKnownMaxPerception = maxPerception;
  176. // Calculate radius based on perception
  177. float radius = maxPerception * perceptionMultiplier;
  178. // Generate circle points
  179. Vector3[] points = new Vector3[circleSegments];
  180. for (int i = 0; i < circleSegments; i++)
  181. {
  182. float angle = (float)i / circleSegments * 2f * Mathf.PI;
  183. points[i] = new Vector3(
  184. Mathf.Cos(angle) * radius,
  185. 0f,
  186. Mathf.Sin(angle) * radius
  187. );
  188. }
  189. // Apply points to LineRenderer
  190. circleRenderer.positionCount = circleSegments;
  191. circleRenderer.SetPositions(points);
  192. // Show the circle
  193. if (!perceptionCircleObject.activeSelf)
  194. {
  195. perceptionCircleObject.SetActive(true);
  196. }
  197. if (showDebugInfo)
  198. {
  199. Debug.Log($"TeamPerceptionVisualizer: Updated perception circle - Max Perception: {maxPerception}, Radius: {radius:F1}");
  200. }
  201. }
  202. /// <summary>
  203. /// Gets the highest perception value among all team members
  204. /// </summary>
  205. /// <returns>The highest perception value, or 0 if no team data available</returns>
  206. private int GetHighestTeamPerception()
  207. {
  208. int maxPerception = 0;
  209. List<TeamCharacter> teamMembers = GetCurrentTeamMembers();
  210. if (teamMembers == null || teamMembers.Count == 0)
  211. return 0;
  212. foreach (TeamCharacter character in teamMembers)
  213. {
  214. if (character != null)
  215. {
  216. int characterPerception = character.FinalPerception; // Use final perception (includes equipment bonuses)
  217. if (characterPerception > maxPerception)
  218. {
  219. maxPerception = characterPerception;
  220. }
  221. }
  222. }
  223. return maxPerception;
  224. }
  225. /// <summary>
  226. /// Gets the current team members from available sources
  227. /// </summary>
  228. /// <returns>List of team members or null if not available</returns>
  229. private List<TeamCharacter> GetCurrentTeamMembers()
  230. {
  231. // Try to get team from MainTeamSelectScript first (if in current scene)
  232. if (teamSelectScript != null)
  233. {
  234. var configuredCharacters = teamSelectScript.GetConfiguredCharacters();
  235. if (configuredCharacters != null && configuredCharacters.Count > 0)
  236. {
  237. if (showDebugInfo)
  238. Debug.Log($"TeamPerceptionVisualizer: Found {configuredCharacters.Count} team members from MainTeamSelectScript");
  239. return configuredCharacters;
  240. }
  241. }
  242. // Fall back to GameStateManager saved data
  243. if (gameStateManager != null && gameStateManager.savedTeam != null)
  244. {
  245. var savedTeamList = new List<TeamCharacter>();
  246. foreach (var character in gameStateManager.savedTeam)
  247. {
  248. if (character != null)
  249. savedTeamList.Add(character);
  250. }
  251. if (savedTeamList.Count > 0)
  252. {
  253. if (showDebugInfo)
  254. Debug.Log($"TeamPerceptionVisualizer: Found {savedTeamList.Count} team members from GameStateManager");
  255. return savedTeamList;
  256. }
  257. }
  258. // Final fallback: Load directly from PlayerPrefs (same as TeamOverviewController)
  259. var playerPrefTeam = new List<TeamCharacter>();
  260. for (int i = 0; i < 4; i++)
  261. {
  262. string prefix = $"Character{i}_";
  263. if (PlayerPrefs.HasKey(prefix + "Exists") && PlayerPrefs.GetInt(prefix + "Exists") == 1)
  264. {
  265. var character = new TeamCharacter();
  266. character.name = PlayerPrefs.GetString(prefix + "Name", "");
  267. character.isMale = PlayerPrefs.GetInt(prefix + "IsMale", 1) == 1;
  268. character.strength = PlayerPrefs.GetInt(prefix + "Strength", 10);
  269. character.dexterity = PlayerPrefs.GetInt(prefix + "Dexterity", 10);
  270. character.constitution = PlayerPrefs.GetInt(prefix + "Constitution", 10);
  271. character.wisdom = PlayerPrefs.GetInt(prefix + "Wisdom", 10);
  272. character.perception = PlayerPrefs.GetInt(prefix + "Perception", 10);
  273. character.gold = PlayerPrefs.GetInt(prefix + "Gold", 25);
  274. playerPrefTeam.Add(character);
  275. }
  276. }
  277. if (playerPrefTeam.Count > 0)
  278. {
  279. if (showDebugInfo)
  280. Debug.Log($"TeamPerceptionVisualizer: Found {playerPrefTeam.Count} team members from PlayerPrefs");
  281. return playerPrefTeam;
  282. }
  283. if (showDebugInfo)
  284. Debug.Log("TeamPerceptionVisualizer: No team members found in any source");
  285. return null;
  286. }
  287. /// <summary>
  288. /// Gets the world position of the team marker
  289. /// </summary>
  290. /// <returns>World position of the team marker, or Vector3.zero if not found</returns>
  291. private Vector3 GetTeamMarkerPosition()
  292. {
  293. if (teamPlacement != null && teamPlacement.TeamMarkerInstance != null)
  294. {
  295. return teamPlacement.TeamMarkerWorldPosition;
  296. }
  297. // Fallback: try to find the marker by name or tag
  298. var teamMarker = GameObject.FindGameObjectWithTag("Player"); // The marker uses "Player" tag
  299. if (teamMarker != null && teamMarker.name == "TeamMarker")
  300. {
  301. return teamMarker.transform.position;
  302. }
  303. // Alternative: look for any GameObject with "TeamMarker" in the name
  304. var foundMarker = GameObject.Find("TeamMarker");
  305. if (foundMarker != null)
  306. {
  307. return foundMarker.transform.position;
  308. }
  309. // If we can't find the marker, return zero
  310. return Vector3.zero;
  311. }
  312. /// <summary>
  313. /// Call this method when team composition or equipment changes
  314. /// </summary>
  315. public void ForceUpdatePerception()
  316. {
  317. lastKnownMaxPerception = -1; // Force update on next check
  318. UpdatePerceptionVisualization();
  319. }
  320. /// <summary>
  321. /// Enables or disables the perception visualization
  322. /// </summary>
  323. /// <param name="enabled">Whether to show the perception circle</param>
  324. public void SetPerceptionVisualizationEnabled(bool enabled)
  325. {
  326. isVisualizationEnabled = enabled;
  327. if (perceptionCircleObject != null)
  328. {
  329. if (enabled)
  330. {
  331. // If enabling, trigger an update to show the circle if appropriate
  332. UpdatePerceptionVisualization();
  333. }
  334. else
  335. {
  336. // If disabling, hide the circle immediately
  337. perceptionCircleObject.SetActive(false);
  338. }
  339. }
  340. }
  341. /// <summary>
  342. /// Updates the circle color
  343. /// </summary>
  344. /// <param name="newColor">New color for the perception circle</param>
  345. public void SetPerceptionColor(Color newColor)
  346. {
  347. perceptionColor = newColor;
  348. if (circleRenderer != null)
  349. {
  350. circleRenderer.startColor = newColor;
  351. circleRenderer.endColor = newColor;
  352. if (circleRenderer.material != null)
  353. {
  354. circleRenderer.material.color = newColor;
  355. }
  356. }
  357. }
  358. /// <summary>
  359. /// Updates the perception multiplier (how large the circle is relative to perception value)
  360. /// </summary>
  361. /// <param name="multiplier">New multiplier value</param>
  362. public void SetPerceptionMultiplier(float multiplier)
  363. {
  364. perceptionMultiplier = multiplier;
  365. ForceUpdatePerception(); // Update circle with new size
  366. }
  367. void OnDestroy()
  368. {
  369. if (Instance == this)
  370. {
  371. Instance = null;
  372. }
  373. }
  374. /// <summary>
  375. /// Debug method to show current perception info
  376. /// </summary>
  377. [ContextMenu("Show Perception Debug Info")]
  378. public void ShowPerceptionDebugInfo()
  379. {
  380. var teamMembers = GetCurrentTeamMembers();
  381. if (teamMembers == null || teamMembers.Count == 0)
  382. {
  383. Debug.Log("No team members found");
  384. return;
  385. }
  386. Debug.Log("=== Team Perception Debug Info ===");
  387. Debug.Log($"Team Members Count: {teamMembers.Count}");
  388. int maxPerception = 0;
  389. string bestCharacter = "";
  390. foreach (var character in teamMembers)
  391. {
  392. if (character != null)
  393. {
  394. int perception = character.FinalPerception;
  395. Debug.Log($"- {character.name}: Perception {perception} (Base: {character.perception}, Modifier: {character.perceptionModifier})");
  396. if (perception > maxPerception)
  397. {
  398. maxPerception = perception;
  399. bestCharacter = character.name;
  400. }
  401. }
  402. }
  403. Debug.Log($"Highest Perception: {maxPerception} ({bestCharacter})");
  404. Debug.Log($"Circle Radius: {maxPerception * perceptionMultiplier:F1} units");
  405. Debug.Log("=== End Debug Info ===");
  406. }
  407. /// <summary>
  408. /// Test method to force immediate visualization update
  409. /// </summary>
  410. [ContextMenu("Force Update Visualization Now")]
  411. public void ForceUpdateVisualizationNow()
  412. {
  413. Debug.Log("=== Forcing Perception Visualization Update ===");
  414. ForceUpdatePerception();
  415. var teamMembers = GetCurrentTeamMembers();
  416. if (teamMembers != null && teamMembers.Count > 0)
  417. {
  418. int maxPerception = GetHighestTeamPerception();
  419. Debug.Log($"Found {teamMembers.Count} team members, highest perception: {maxPerception}");
  420. if (perceptionCircleObject != null)
  421. {
  422. Debug.Log($"Circle active: {perceptionCircleObject.activeSelf}");
  423. Debug.Log($"Circle position: {perceptionCircleObject.transform.position}");
  424. }
  425. else
  426. {
  427. Debug.LogWarning("Perception circle object is null!");
  428. }
  429. }
  430. else
  431. {
  432. Debug.LogWarning("No team members found for visualization!");
  433. }
  434. Debug.Log("=== End Force Update ===");
  435. }
  436. }