EventMarkerVisualizer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. /// <summary>
  5. /// Manages spottable event markers that appear as red dots within the team's perception range
  6. /// </summary>
  7. public class EventMarkerVisualizer : MonoBehaviour
  8. {
  9. [Header("Event Marker Settings")]
  10. [SerializeField] private GameObject eventMarkerPrefab;
  11. [SerializeField] private Material eventMarkerMaterial;
  12. [SerializeField] private Color eventMarkerColor = Color.red;
  13. [SerializeField] private float markerSize = 0.8f;
  14. [SerializeField] private float markerHeight = 0.6f;
  15. [Header("Spawn Settings")]
  16. [SerializeField] private int maxActiveMarkers = 5;
  17. [SerializeField] private float minDistanceFromTeam = 2f;
  18. [SerializeField] private float maxDistanceFromTeam = 8f;
  19. [SerializeField] private float markerLifetime = 30f; // Markers disappear after 30 seconds if not approached
  20. [Header("Visual Effects")]
  21. [SerializeField] private bool enablePulsing = true;
  22. [SerializeField] private float pulseSpeed = 2f;
  23. [SerializeField] private float pulseIntensity = 0.3f;
  24. [Header("Debug")]
  25. [SerializeField] private bool showDebugInfo = false;
  26. // Component references
  27. private TeamPerceptionVisualizer perceptionVisualizer;
  28. private SimpleTeamPlacement teamPlacement;
  29. private TravelEventSystem eventSystem;
  30. private MapMaker2 mapMaker;
  31. // Active markers tracking
  32. private List<EventMarker> activeMarkers = new List<EventMarker>();
  33. private List<Vector2Int> occupiedPositions = new List<Vector2Int>();
  34. public static EventMarkerVisualizer Instance { get; private set; }
  35. void Awake()
  36. {
  37. if (Instance == null)
  38. {
  39. Instance = this;
  40. }
  41. else
  42. {
  43. Debug.LogWarning("Multiple EventMarkerVisualizer instances found. Destroying this one.");
  44. Destroy(gameObject);
  45. return;
  46. }
  47. }
  48. void Start()
  49. {
  50. // Find required components
  51. perceptionVisualizer = FindFirstObjectByType<TeamPerceptionVisualizer>();
  52. teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
  53. eventSystem = FindFirstObjectByType<TravelEventSystem>();
  54. mapMaker = FindFirstObjectByType<MapMaker2>();
  55. if (perceptionVisualizer == null)
  56. {
  57. Debug.LogError("EventMarkerVisualizer: TeamPerceptionVisualizer not found!");
  58. }
  59. if (teamPlacement == null)
  60. {
  61. Debug.LogError("EventMarkerVisualizer: SimpleTeamPlacement not found!");
  62. }
  63. }
  64. void Update()
  65. {
  66. // Clean up expired markers
  67. CleanupExpiredMarkers();
  68. // Update marker visuals (pulsing effect)
  69. if (enablePulsing)
  70. {
  71. UpdateMarkerPulsing();
  72. }
  73. }
  74. /// <summary>
  75. /// Attempts to spawn a spottable event marker within perception range
  76. /// </summary>
  77. /// <param name="travelEvent">The event to create a marker for</param>
  78. /// <returns>True if marker was successfully spawned</returns>
  79. public bool TrySpawnEventMarker(TravelEvent travelEvent)
  80. {
  81. if (travelEvent == null || activeMarkers.Count >= maxActiveMarkers)
  82. {
  83. return false;
  84. }
  85. // Get team position and perception range
  86. Vector2Int teamPosition = GetTeamPosition();
  87. float perceptionRange = GetTeamPerceptionRange();
  88. if (perceptionRange <= 0)
  89. {
  90. if (showDebugInfo)
  91. Debug.Log("EventMarkerVisualizer: No perception range, cannot spawn marker");
  92. return false;
  93. }
  94. // Find a valid position within perception range
  95. Vector2Int markerPosition = FindValidMarkerPosition(teamPosition, perceptionRange);
  96. if (markerPosition == Vector2Int.zero)
  97. {
  98. if (showDebugInfo)
  99. Debug.Log("EventMarkerVisualizer: Could not find valid position for event marker");
  100. return false;
  101. }
  102. // Create the marker
  103. GameObject markerObject = CreateMarkerObject(markerPosition);
  104. if (markerObject == null)
  105. {
  106. return false;
  107. }
  108. // Create event marker data
  109. EventMarker marker = new EventMarker
  110. {
  111. gameObject = markerObject,
  112. travelEvent = travelEvent,
  113. position = markerPosition,
  114. spawnTime = Time.time,
  115. isActive = true
  116. };
  117. activeMarkers.Add(marker);
  118. occupiedPositions.Add(markerPosition);
  119. if (showDebugInfo)
  120. {
  121. Debug.Log($"EventMarkerVisualizer: Spawned marker for '{travelEvent.eventName}' at {markerPosition}");
  122. }
  123. return true;
  124. }
  125. /// <summary>
  126. /// Checks if the team is close enough to trigger an event marker
  127. /// </summary>
  128. /// <param name="teamPosition">Current team position</param>
  129. /// <returns>The event to trigger, or null if none</returns>
  130. public TravelEvent CheckForMarkerTrigger(Vector2Int teamPosition)
  131. {
  132. float triggerDistance = 1.5f; // Distance to trigger event
  133. foreach (var marker in activeMarkers.ToList())
  134. {
  135. if (!marker.isActive) continue;
  136. float distance = Vector2Int.Distance(teamPosition, marker.position);
  137. if (distance <= triggerDistance)
  138. {
  139. // Remove the marker and return the event
  140. RemoveMarker(marker);
  141. if (showDebugInfo)
  142. {
  143. Debug.Log($"EventMarkerVisualizer: Team triggered event '{marker.travelEvent.eventName}' at {marker.position}");
  144. }
  145. return marker.travelEvent;
  146. }
  147. }
  148. return null;
  149. }
  150. /// <summary>
  151. /// Removes all active event markers
  152. /// </summary>
  153. public void ClearAllMarkers()
  154. {
  155. foreach (var marker in activeMarkers.ToList())
  156. {
  157. RemoveMarker(marker);
  158. }
  159. }
  160. private Vector2Int GetTeamPosition()
  161. {
  162. if (teamPlacement != null)
  163. {
  164. return teamPlacement.GetCurrentTeamPosition();
  165. }
  166. return Vector2Int.zero;
  167. }
  168. private float GetTeamPerceptionRange()
  169. {
  170. if (perceptionVisualizer != null)
  171. {
  172. // Access the perception range from the visualizer
  173. // We need to get the team's highest perception and apply the multiplier
  174. return GetHighestTeamPerception() * 1.0f; // Use same multiplier as perception visualizer
  175. }
  176. return 0f;
  177. }
  178. private int GetHighestTeamPerception()
  179. {
  180. // This mirrors the logic from TeamPerceptionVisualizer
  181. int maxPerception = 0;
  182. List<TeamCharacter> teamMembers = GetCurrentTeamMembers();
  183. if (teamMembers == null || teamMembers.Count == 0)
  184. return 0;
  185. foreach (TeamCharacter character in teamMembers)
  186. {
  187. if (character != null)
  188. {
  189. int characterPerception = character.FinalPerception;
  190. if (characterPerception > maxPerception)
  191. {
  192. maxPerception = characterPerception;
  193. }
  194. }
  195. }
  196. return maxPerception;
  197. }
  198. private List<TeamCharacter> GetCurrentTeamMembers()
  199. {
  200. // Use the same logic as TeamPerceptionVisualizer
  201. var teamSelectScript = FindFirstObjectByType<MainTeamSelectScript>();
  202. var gameStateManager = FindFirstObjectByType<GameStateManager>();
  203. // Try to get team from MainTeamSelectScript first
  204. if (teamSelectScript != null)
  205. {
  206. var configuredCharacters = teamSelectScript.GetConfiguredCharacters();
  207. if (configuredCharacters != null && configuredCharacters.Count > 0)
  208. {
  209. return configuredCharacters;
  210. }
  211. }
  212. // Fall back to GameStateManager
  213. if (gameStateManager != null && gameStateManager.savedTeam != null)
  214. {
  215. var savedTeamList = new List<TeamCharacter>();
  216. foreach (var character in gameStateManager.savedTeam)
  217. {
  218. if (character != null)
  219. savedTeamList.Add(character);
  220. }
  221. if (savedTeamList.Count > 0)
  222. {
  223. return savedTeamList;
  224. }
  225. }
  226. // Final fallback: Load from PlayerPrefs
  227. var playerPrefTeam = new List<TeamCharacter>();
  228. for (int i = 0; i < 4; i++)
  229. {
  230. string prefix = $"Character{i}_";
  231. if (PlayerPrefs.HasKey(prefix + "Exists") && PlayerPrefs.GetInt(prefix + "Exists") == 1)
  232. {
  233. var character = new TeamCharacter();
  234. character.name = PlayerPrefs.GetString(prefix + "Name", "");
  235. character.isMale = PlayerPrefs.GetInt(prefix + "IsMale", 1) == 1;
  236. character.strength = PlayerPrefs.GetInt(prefix + "Strength", 10);
  237. character.dexterity = PlayerPrefs.GetInt(prefix + "Dexterity", 10);
  238. character.constitution = PlayerPrefs.GetInt(prefix + "Constitution", 10);
  239. character.wisdom = PlayerPrefs.GetInt(prefix + "Wisdom", 10);
  240. character.perception = PlayerPrefs.GetInt(prefix + "Perception", 10);
  241. character.gold = PlayerPrefs.GetInt(prefix + "Gold", 25);
  242. playerPrefTeam.Add(character);
  243. }
  244. }
  245. return playerPrefTeam.Count > 0 ? playerPrefTeam : null;
  246. }
  247. private Vector2Int FindValidMarkerPosition(Vector2Int teamPosition, float perceptionRange)
  248. {
  249. int maxAttempts = 20;
  250. for (int i = 0; i < maxAttempts; i++)
  251. {
  252. // Generate random position within perception range
  253. float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
  254. float distance = Random.Range(minDistanceFromTeam, Mathf.Min(maxDistanceFromTeam, perceptionRange));
  255. Vector2Int candidatePosition = new Vector2Int(
  256. teamPosition.x + Mathf.RoundToInt(Mathf.Cos(angle) * distance),
  257. teamPosition.y + Mathf.RoundToInt(Mathf.Sin(angle) * distance)
  258. );
  259. // Check if position is valid
  260. if (IsValidMarkerPosition(candidatePosition))
  261. {
  262. return candidatePosition;
  263. }
  264. }
  265. return Vector2Int.zero; // Failed to find valid position
  266. }
  267. private bool IsValidMarkerPosition(Vector2Int position)
  268. {
  269. // Check if position is already occupied
  270. if (occupiedPositions.Contains(position))
  271. {
  272. return false;
  273. }
  274. // Check if position is valid on the map
  275. if (mapMaker != null && mapMaker.GetMapData() != null)
  276. {
  277. return mapMaker.GetMapData().IsValidPosition(position.x, position.y);
  278. }
  279. return true; // Default to valid if no map check available
  280. }
  281. private GameObject CreateMarkerObject(Vector2Int position)
  282. {
  283. GameObject markerObject;
  284. if (eventMarkerPrefab != null)
  285. {
  286. markerObject = Instantiate(eventMarkerPrefab);
  287. }
  288. else
  289. {
  290. // Create simple sphere marker
  291. markerObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
  292. markerObject.transform.localScale = Vector3.one * markerSize;
  293. // Remove collider to avoid interference
  294. var collider = markerObject.GetComponent<Collider>();
  295. if (collider != null)
  296. {
  297. DestroyImmediate(collider);
  298. }
  299. }
  300. // Position the marker
  301. Vector3 worldPosition = GetWorldPosition(position);
  302. markerObject.transform.position = worldPosition;
  303. // Apply material and color
  304. ApplyMarkerMaterial(markerObject);
  305. // Set name and tag
  306. markerObject.name = "EventMarker";
  307. // Use default "Untagged" tag to avoid tag definition errors
  308. markerObject.tag = "Untagged";
  309. return markerObject;
  310. }
  311. private Vector3 GetWorldPosition(Vector2Int mapPosition)
  312. {
  313. // Convert map coordinates to world position
  314. float tileSize = 1f;
  315. if (mapMaker?.mapVisualizer != null)
  316. {
  317. tileSize = mapMaker.mapVisualizer.tileSize;
  318. }
  319. return new Vector3(
  320. mapPosition.x * tileSize,
  321. markerHeight,
  322. mapPosition.y * tileSize
  323. );
  324. }
  325. private void ApplyMarkerMaterial(GameObject markerObject)
  326. {
  327. Renderer renderer = markerObject.GetComponent<Renderer>();
  328. if (renderer == null) return;
  329. if (eventMarkerMaterial != null)
  330. {
  331. renderer.material = eventMarkerMaterial;
  332. }
  333. else
  334. {
  335. // Create a simple red material
  336. Material material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
  337. if (material.shader == null)
  338. {
  339. material = new Material(Shader.Find("Standard"));
  340. }
  341. material.color = eventMarkerColor;
  342. // Add emission for visibility
  343. if (material.HasProperty("_EmissionColor"))
  344. {
  345. material.EnableKeyword("_EMISSION");
  346. material.SetColor("_EmissionColor", eventMarkerColor * 0.5f);
  347. }
  348. renderer.material = material;
  349. }
  350. }
  351. private void CleanupExpiredMarkers()
  352. {
  353. var markersToRemove = activeMarkers.Where(m =>
  354. !m.isActive ||
  355. m.gameObject == null ||
  356. (Time.time - m.spawnTime) > markerLifetime
  357. ).ToList();
  358. foreach (var marker in markersToRemove)
  359. {
  360. RemoveMarker(marker);
  361. }
  362. }
  363. private void UpdateMarkerPulsing()
  364. {
  365. float pulseValue = (Mathf.Sin(Time.time * pulseSpeed) + 1f) * 0.5f; // 0 to 1
  366. float scale = 1f + (pulseValue * pulseIntensity);
  367. foreach (var marker in activeMarkers)
  368. {
  369. if (marker.isActive && marker.gameObject != null)
  370. {
  371. marker.gameObject.transform.localScale = Vector3.one * (markerSize * scale);
  372. }
  373. }
  374. }
  375. private void RemoveMarker(EventMarker marker)
  376. {
  377. if (marker.gameObject != null)
  378. {
  379. DestroyImmediate(marker.gameObject);
  380. }
  381. activeMarkers.Remove(marker);
  382. occupiedPositions.Remove(marker.position);
  383. marker.isActive = false;
  384. }
  385. /// <summary>
  386. /// Gets the number of currently active event markers
  387. /// </summary>
  388. public int GetActiveMarkerCount()
  389. {
  390. return activeMarkers.Count(m => m.isActive);
  391. }
  392. /// <summary>
  393. /// Debug method to show all active markers
  394. /// </summary>
  395. [ContextMenu("Show Active Markers")]
  396. public void ShowActiveMarkers()
  397. {
  398. Debug.Log($"=== Active Event Markers ({activeMarkers.Count}) ===");
  399. foreach (var marker in activeMarkers)
  400. {
  401. if (marker.isActive)
  402. {
  403. float age = Time.time - marker.spawnTime;
  404. Debug.Log($"- {marker.travelEvent.eventName} at {marker.position} (age: {age:F1}s)");
  405. }
  406. }
  407. }
  408. }
  409. /// <summary>
  410. /// Data structure for tracking active event markers
  411. /// </summary>
  412. [System.Serializable]
  413. public class EventMarker
  414. {
  415. public GameObject gameObject;
  416. public TravelEvent travelEvent;
  417. public Vector2Int position;
  418. public float spawnTime;
  419. public bool isActive;
  420. }