Prechádzať zdrojové kódy

Spottable events on the map

Axel Nordh 8 mesiacov pred
rodič
commit
48b697022f

+ 180 - 0
Assets/Scripts/Events/PerceptionEventSystemSetup.cs

@@ -0,0 +1,180 @@
+using UnityEngine;
+
+/// <summary>
+/// Setup script for the enhanced perception and event system
+/// Automatically adds required components and configures the spottable event system
+/// </summary>
+public class PerceptionEventSystemSetup : MonoBehaviour
+{
+    [Header("Auto Setup")]
+    [SerializeField] private bool autoSetupOnStart = true;
+    
+    [Header("Event Marker Settings")]
+    [SerializeField] private GameObject eventMarkerPrefab;
+    [SerializeField] private Material eventMarkerMaterial;
+    [SerializeField] private Color eventMarkerColor = Color.red;
+    [SerializeField] private int maxActiveMarkers = 5;
+    
+    [Header("Debug")]
+    [SerializeField] private bool showSetupLogs = true;
+    
+    void Start()
+    {
+        if (autoSetupOnStart)
+        {
+            SetupPerceptionEventSystem();
+        }
+    }
+    
+    /// <summary>
+    /// Sets up the perception and event system components
+    /// </summary>
+    [ContextMenu("Setup Perception Event System")]
+    public void SetupPerceptionEventSystem()
+    {
+        if (showSetupLogs)
+            Debug.Log("🔧 Setting up Perception Event System...");
+        
+        // Ensure TeamPerceptionVisualizer exists
+        SetupPerceptionVisualizer();
+        
+        // Setup EventMarkerVisualizer
+        SetupEventMarkerVisualizer();
+        
+        // Verify TravelEventSystem integration
+        VerifyTravelEventSystem();
+        
+        if (showSetupLogs)
+            Debug.Log("✅ Perception Event System setup complete!");
+    }
+    
+    private void SetupPerceptionVisualizer()
+    {
+        var perceptionVisualizer = FindFirstObjectByType<TeamPerceptionVisualizer>();
+        if (perceptionVisualizer == null)
+        {
+            // Find a suitable GameObject to add it to (prefer the one with SimpleTeamPlacement)
+            var teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
+            GameObject targetObject = teamPlacement?.gameObject ?? gameObject;
+            
+            perceptionVisualizer = targetObject.AddComponent<TeamPerceptionVisualizer>();
+            if (showSetupLogs)
+                Debug.Log($"✅ Added TeamPerceptionVisualizer to {targetObject.name}");
+        }
+        else if (showSetupLogs)
+        {
+            Debug.Log("✅ TeamPerceptionVisualizer already exists");
+        }
+    }
+    
+    private void SetupEventMarkerVisualizer()
+    {
+        var markerVisualizer = FindFirstObjectByType<EventMarkerVisualizer>();
+        if (markerVisualizer == null)
+        {
+            // Find a suitable GameObject to add it to (prefer the one with TravelEventSystem)
+            var eventSystem = FindFirstObjectByType<TravelEventSystem>();
+            GameObject targetObject = eventSystem?.gameObject ?? gameObject;
+            
+            markerVisualizer = targetObject.AddComponent<EventMarkerVisualizer>();
+            
+            // Configure with our settings
+            if (eventMarkerPrefab != null)
+            {
+                // Use reflection to set private fields since they're SerializeField
+                var prefabField = markerVisualizer.GetType().GetField("eventMarkerPrefab", 
+                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+                prefabField?.SetValue(markerVisualizer, eventMarkerPrefab);
+            }
+            
+            if (eventMarkerMaterial != null)
+            {
+                var materialField = markerVisualizer.GetType().GetField("eventMarkerMaterial", 
+                    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+                materialField?.SetValue(markerVisualizer, eventMarkerMaterial);
+            }
+            
+            var colorField = markerVisualizer.GetType().GetField("eventMarkerColor", 
+                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+            colorField?.SetValue(markerVisualizer, eventMarkerColor);
+            
+            var maxMarkersField = markerVisualizer.GetType().GetField("maxActiveMarkers", 
+                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+            maxMarkersField?.SetValue(markerVisualizer, maxActiveMarkers);
+            
+            if (showSetupLogs)
+                Debug.Log($"✅ Added EventMarkerVisualizer to {targetObject.name}");
+        }
+        else if (showSetupLogs)
+        {
+            Debug.Log("✅ EventMarkerVisualizer already exists");
+        }
+    }
+    
+    private void VerifyTravelEventSystem()
+    {
+        var eventSystem = FindFirstObjectByType<TravelEventSystem>();
+        if (eventSystem == null)
+        {
+            if (showSetupLogs)
+                Debug.LogWarning("⚠️ No TravelEventSystem found. Please add one to use the event system.");
+            return;
+        }
+        
+        if (showSetupLogs)
+            Debug.Log("✅ TravelEventSystem found and should integrate automatically");
+    }
+    
+    /// <summary>
+    /// Creates example spottable events for testing
+    /// </summary>
+    [ContextMenu("Create Example Spottable Events")]
+    public void CreateExampleSpottableEvents()
+    {
+        if (showSetupLogs)
+            Debug.Log("🎭 Creating example spottable events...");
+        
+        // This would create ScriptableObject assets in the project
+        // For now, just log what would be created
+        Debug.Log("📋 Example events to create:");
+        Debug.Log("- SpottableSkeletonPatrol: Skeleton warriors that can be spotted and avoided");
+        Debug.Log("- SpottableBanditCamp: Bandit camps visible from a distance");
+        Debug.Log("- SpottableTreasureCache: Hidden treasure that can be spotted with good perception");
+        
+        Debug.Log("💡 Tip: Right-click in Project → Create → RPG → Travel Events → Spottable → [Event Type]");
+    }
+    
+    /// <summary>
+    /// Shows current system status
+    /// </summary>
+    [ContextMenu("Show System Status")]
+    public void ShowSystemStatus()
+    {
+        Debug.Log("=== Perception Event System Status ===");
+        
+        var perceptionVisualizer = FindFirstObjectByType<TeamPerceptionVisualizer>();
+        Debug.Log($"TeamPerceptionVisualizer: {(perceptionVisualizer ? "✅ Found" : "❌ Missing")}");
+        
+        var markerVisualizer = FindFirstObjectByType<EventMarkerVisualizer>();
+        Debug.Log($"EventMarkerVisualizer: {(markerVisualizer ? "✅ Found" : "❌ Missing")}");
+        
+        var eventSystem = FindFirstObjectByType<TravelEventSystem>();
+        Debug.Log($"TravelEventSystem: {(eventSystem ? "✅ Found" : "❌ Missing")}");
+        
+        var teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
+        Debug.Log($"SimpleTeamPlacement: {(teamPlacement ? "✅ Found" : "❌ Missing")}");
+        
+        if (markerVisualizer != null)
+        {
+            // Use reflection to get active marker count
+            var method = markerVisualizer.GetType().GetMethod("GetActiveMarkerCount");
+            if (method != null)
+            {
+                int activeMarkers = (int)method.Invoke(markerVisualizer, null);
+                Debug.Log($"Active Event Markers: {activeMarkers}");
+            }
+        }
+        
+        Debug.Log("========================================");
+    }
+}

+ 88 - 0
Assets/Scripts/Events/SpottableSkeletonPatrol.cs

@@ -0,0 +1,88 @@
+using UnityEngine;
+
+/// <summary>
+/// A spottable skeleton patrol event that appears as a red marker within perception range
+/// Players can choose to approach and engage or avoid the patrol
+/// </summary>
+[CreateAssetMenu(fileName = "SpottableSkeletonPatrol", menuName = "RPG/Travel Events/Spottable/Skeleton Patrol")]
+public class SpottableSkeletonPatrol : CombatTravelEvent
+{
+    [Header("Spottable Event Settings")]
+    [TextArea(2, 4)]
+    public string[] spotDescription = {
+        "You spot skeletal figures moving in the distance...",
+        "A patrol of undead warriors can be seen ahead...",
+        "Your keen eyes detect a group of skeleton warriors patrolling the area..."
+    };
+    
+    void OnEnable()
+    {
+        // Configure as spottable event
+        triggerType = EventTriggerType.Spottable;
+        
+        // Set up the event details
+        eventName = "Skeleton Patrol";
+        eventDescription = "A patrol of skeleton warriors guards this area. They haven't noticed you yet.";
+        eventType = EventType.Combat;
+        rarity = EventRarity.Common;
+        
+        // Configure combat settings
+        minEnemies = 2;
+        maxEnemies = 4;
+        allowRunningAway = true;
+        runAwaySuccessChance = 0.85f; // Higher chance since you can see them coming
+        runAwayHealthPenalty = 2; // Lower penalty since you're prepared
+        
+        // Set terrain preferences (skeletons prefer ruins, forests, mountains)
+        plainsChance = 0.4f;
+        forestChance = 0.8f;
+        mountainChance = 0.7f;
+        roadChance = 0.3f; // Less likely on roads
+        townChance = 0.1f; // Very unlikely in settlements
+        villageChance = 0.2f;
+        
+        // Configure encounter descriptions
+        encounterDescriptions = new string[]
+        {
+            "The skeleton patrol notices your approach and readies their weapons!",
+            "The undead warriors turn toward you with hollow, glowing eyes!",
+            "The skeleton patrol forms a battle line as you draw near!"
+        };
+        
+        // Configure escape messages for spotted encounters
+        successfulRunAwayMessages = new string[]
+        {
+            "Having spotted them first, your party easily retreats unnoticed!",
+            "Your party backs away quietly before the patrol spots you!",
+            "You successfully avoid the skeleton patrol by taking a different route!"
+        };
+        
+        failedRunAwayMessages = new string[]
+        {
+            "The skeletons spot your retreat! You take {damage} damage as they give chase!",
+            "Your withdrawal is noticed! The patrol pursues, dealing {damage} damage!",
+            "The undead patrol catches up to your fleeing party for {damage} damage!"
+        };
+    }
+    
+    public override EventResult ExecuteEvent(TravelEventContext context)
+    {
+        // Override the base execution to provide special messaging for spottable events
+        Debug.Log($"🔍 Executing spottable skeleton patrol at {context.currentPosition}");
+        
+        // Show spot description first
+        string description = spotDescription[Random.Range(0, spotDescription.Length)];
+        Debug.Log($"👁️ Spot description: {description}");
+        
+        // Then execute as normal combat event
+        return base.ExecuteEvent(context);
+    }
+    
+    /// <summary>
+    /// Provides a description of what the team spotted
+    /// </summary>
+    public string GetSpotDescription()
+    {
+        return spotDescription[Random.Range(0, spotDescription.Length)];
+    }
+}

+ 21 - 0
Assets/Scripts/Events/TravelEvent.cs

@@ -1,6 +1,23 @@
 using UnityEngine;
 using System;
 
+/// <summary>
+/// Defines how an event can be triggered during travel
+/// </summary>
+[System.Serializable]
+public enum EventTriggerType
+{
+    /// <summary>
+    /// Event triggers automatically when conditions are met (legacy behavior)
+    /// </summary>
+    Automatic,
+    
+    /// <summary>
+    /// Event appears as a red marker within perception range that can be approached
+    /// </summary>
+    Spottable
+}
+
 /// <summary>
 /// Base class for all travel events that can occur during journeys
 /// </summary>
@@ -13,6 +30,10 @@ public abstract class TravelEvent : ScriptableObject
     public string eventDescription;
     public EventType eventType;
     public EventRarity rarity = EventRarity.Common;
+    
+    [Header("Trigger Behavior")]
+    [Tooltip("How this event is triggered - automatically or as a spottable marker")]
+    public EventTriggerType triggerType = EventTriggerType.Automatic;
 
     [Header("Terrain Preferences")]
     [Range(0f, 1f)]

+ 65 - 4
Assets/Scripts/Events/TravelEventSystem.cs

@@ -34,6 +34,7 @@ public class TravelEventSystem : MonoBehaviour
     private MapMaker2 mapMaker;
     private SimpleTeamPlacement teamPlacement;
     private TravelEventUI eventUI;
+    private MonoBehaviour markerVisualizer; // Use MonoBehaviour to avoid compilation issues
     // Enhanced system will be linked after compilation
     private object enhancedEventSystem;
 
@@ -44,6 +45,12 @@ public class TravelEventSystem : MonoBehaviour
         mapMaker = FindFirstObjectByType<MapMaker2>();
         teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
         eventUI = FindFirstObjectByType<TravelEventUI>();
+        
+        // Find EventMarkerVisualizer using reflection to avoid compilation order issues
+        var markerVisualizerComponents = FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
+        markerVisualizer = System.Array.Find(markerVisualizerComponents,
+            comp => comp.GetType().Name == "EventMarkerVisualizer");
+        
         // Enhanced system will be found after compilation
         // enhancedEventSystem = FindFirstObjectByType<EnhancedTravelEventSystem>();
 
@@ -88,6 +95,9 @@ public class TravelEventSystem : MonoBehaviour
                 Debug.Log($"🚶 Team moved from {lastTeamPosition} to {currentTeamPosition} (tiles since last check: {tilesTraveledSinceLastCheck})");
             }
 
+            // Check for event marker triggers first
+            CheckForEventMarkerTriggers(currentTeamPosition);
+
             lastTeamPosition = currentTeamPosition;
 
             // Check for events every N tiles traveled
@@ -145,6 +155,30 @@ public class TravelEventSystem : MonoBehaviour
         ExecuteEvent(eventToTrigger, currentContext);
     }
 
+    /// <summary>
+    /// Check if team has triggered any event markers
+    /// </summary>
+    private void CheckForEventMarkerTriggers(Vector2Int teamPosition)
+    {
+        if (markerVisualizer == null) return;
+
+        // Use reflection to call CheckForMarkerTrigger method
+        var method = markerVisualizer.GetType().GetMethod("CheckForMarkerTrigger");
+        if (method != null)
+        {
+            var triggeredEvent = method.Invoke(markerVisualizer, new object[] { teamPosition }) as TravelEvent;
+            if (triggeredEvent != null)
+            {
+                // Create context and execute the triggered event
+                currentContext = CreateCurrentContext();
+                if (currentContext != null)
+                {
+                    ExecuteEvent(triggeredEvent, currentContext);
+                }
+            }
+        }
+    }
+
     /// <summary>
     /// Trigger a random event based on current context
     /// </summary>
@@ -158,11 +192,38 @@ public class TravelEventSystem : MonoBehaviour
             return;
         }
 
-        // Select event based on weighted chances
-        TravelEvent selectedEvent = SelectWeightedEvent(possibleEvents, context);
-        if (selectedEvent != null)
+        // Separate events by trigger type
+        var automaticEvents = possibleEvents.Where(e => e.triggerType == EventTriggerType.Automatic).ToList();
+        var spottableEvents = possibleEvents.Where(e => e.triggerType == EventTriggerType.Spottable).ToList();
+
+        // Try to trigger automatic event first
+        if (automaticEvents.Count > 0)
+        {
+            TravelEvent selectedEvent = SelectWeightedEvent(automaticEvents, context);
+            if (selectedEvent != null)
+            {
+                ExecuteEvent(selectedEvent, context);
+                return;
+            }
+        }
+
+        // Try to spawn spottable event markers
+        if (spottableEvents.Count > 0 && markerVisualizer != null)
         {
-            ExecuteEvent(selectedEvent, context);
+            TravelEvent selectedSpottableEvent = SelectWeightedEvent(spottableEvents, context);
+            if (selectedSpottableEvent != null)
+            {
+                // Use reflection to call TrySpawnEventMarker method
+                var spawnMethod = markerVisualizer.GetType().GetMethod("TrySpawnEventMarker");
+                if (spawnMethod != null)
+                {
+                    var markerSpawned = (bool)spawnMethod.Invoke(markerVisualizer, new object[] { selectedSpottableEvent });
+                    if (markerSpawned && showDebugLogs)
+                    {
+                        Debug.Log($"🔴 Spawned spottable event marker: {selectedSpottableEvent.eventName}");
+                    }
+                }
+            }
         }
     }
 

+ 493 - 0
Assets/Scripts/Map/EventMarkerVisualizer.cs

@@ -0,0 +1,493 @@
+using UnityEngine;
+using System.Collections.Generic;
+using System.Linq;
+
+/// <summary>
+/// Manages spottable event markers that appear as red dots within the team's perception range
+/// </summary>
+public class EventMarkerVisualizer : MonoBehaviour
+{
+    [Header("Event Marker Settings")]
+    [SerializeField] private GameObject eventMarkerPrefab;
+    [SerializeField] private Material eventMarkerMaterial;
+    [SerializeField] private Color eventMarkerColor = Color.red;
+    [SerializeField] private float markerSize = 0.8f;
+    [SerializeField] private float markerHeight = 0.6f;
+    
+    [Header("Spawn Settings")]
+    [SerializeField] private int maxActiveMarkers = 5;
+    [SerializeField] private float minDistanceFromTeam = 2f;
+    [SerializeField] private float maxDistanceFromTeam = 8f;
+    [SerializeField] private float markerLifetime = 30f; // Markers disappear after 30 seconds if not approached
+    
+    [Header("Visual Effects")]
+    [SerializeField] private bool enablePulsing = true;
+    [SerializeField] private float pulseSpeed = 2f;
+    [SerializeField] private float pulseIntensity = 0.3f;
+    
+    [Header("Debug")]
+    [SerializeField] private bool showDebugInfo = false;
+    
+    // Component references
+    private TeamPerceptionVisualizer perceptionVisualizer;
+    private SimpleTeamPlacement teamPlacement;
+    private TravelEventSystem eventSystem;
+    private MapMaker2 mapMaker;
+    
+    // Active markers tracking
+    private List<EventMarker> activeMarkers = new List<EventMarker>();
+    private List<Vector2Int> occupiedPositions = new List<Vector2Int>();
+    
+    public static EventMarkerVisualizer Instance { get; private set; }
+    
+    void Awake()
+    {
+        if (Instance == null)
+        {
+            Instance = this;
+        }
+        else
+        {
+            Debug.LogWarning("Multiple EventMarkerVisualizer instances found. Destroying this one.");
+            Destroy(gameObject);
+            return;
+        }
+    }
+    
+    void Start()
+    {
+        // Find required components
+        perceptionVisualizer = FindFirstObjectByType<TeamPerceptionVisualizer>();
+        teamPlacement = FindFirstObjectByType<SimpleTeamPlacement>();
+        eventSystem = FindFirstObjectByType<TravelEventSystem>();
+        mapMaker = FindFirstObjectByType<MapMaker2>();
+        
+        if (perceptionVisualizer == null)
+        {
+            Debug.LogError("EventMarkerVisualizer: TeamPerceptionVisualizer not found!");
+        }
+        
+        if (teamPlacement == null)
+        {
+            Debug.LogError("EventMarkerVisualizer: SimpleTeamPlacement not found!");
+        }
+    }
+    
+    void Update()
+    {
+        // Clean up expired markers
+        CleanupExpiredMarkers();
+        
+        // Update marker visuals (pulsing effect)
+        if (enablePulsing)
+        {
+            UpdateMarkerPulsing();
+        }
+    }
+    
+    /// <summary>
+    /// Attempts to spawn a spottable event marker within perception range
+    /// </summary>
+    /// <param name="travelEvent">The event to create a marker for</param>
+    /// <returns>True if marker was successfully spawned</returns>
+    public bool TrySpawnEventMarker(TravelEvent travelEvent)
+    {
+        if (travelEvent == null || activeMarkers.Count >= maxActiveMarkers)
+        {
+            return false;
+        }
+        
+        // Get team position and perception range
+        Vector2Int teamPosition = GetTeamPosition();
+        float perceptionRange = GetTeamPerceptionRange();
+        
+        if (perceptionRange <= 0)
+        {
+            if (showDebugInfo)
+                Debug.Log("EventMarkerVisualizer: No perception range, cannot spawn marker");
+            return false;
+        }
+        
+        // Find a valid position within perception range
+        Vector2Int markerPosition = FindValidMarkerPosition(teamPosition, perceptionRange);
+        if (markerPosition == Vector2Int.zero)
+        {
+            if (showDebugInfo)
+                Debug.Log("EventMarkerVisualizer: Could not find valid position for event marker");
+            return false;
+        }
+        
+        // Create the marker
+        GameObject markerObject = CreateMarkerObject(markerPosition);
+        if (markerObject == null)
+        {
+            return false;
+        }
+        
+        // Create event marker data
+        EventMarker marker = new EventMarker
+        {
+            gameObject = markerObject,
+            travelEvent = travelEvent,
+            position = markerPosition,
+            spawnTime = Time.time,
+            isActive = true
+        };
+        
+        activeMarkers.Add(marker);
+        occupiedPositions.Add(markerPosition);
+        
+        if (showDebugInfo)
+        {
+            Debug.Log($"EventMarkerVisualizer: Spawned marker for '{travelEvent.eventName}' at {markerPosition}");
+        }
+        
+        return true;
+    }
+    
+    /// <summary>
+    /// Checks if the team is close enough to trigger an event marker
+    /// </summary>
+    /// <param name="teamPosition">Current team position</param>
+    /// <returns>The event to trigger, or null if none</returns>
+    public TravelEvent CheckForMarkerTrigger(Vector2Int teamPosition)
+    {
+        float triggerDistance = 1.5f; // Distance to trigger event
+        
+        foreach (var marker in activeMarkers.ToList())
+        {
+            if (!marker.isActive) continue;
+            
+            float distance = Vector2Int.Distance(teamPosition, marker.position);
+            if (distance <= triggerDistance)
+            {
+                // Remove the marker and return the event
+                RemoveMarker(marker);
+                
+                if (showDebugInfo)
+                {
+                    Debug.Log($"EventMarkerVisualizer: Team triggered event '{marker.travelEvent.eventName}' at {marker.position}");
+                }
+                
+                return marker.travelEvent;
+            }
+        }
+        
+        return null;
+    }
+    
+    /// <summary>
+    /// Removes all active event markers
+    /// </summary>
+    public void ClearAllMarkers()
+    {
+        foreach (var marker in activeMarkers.ToList())
+        {
+            RemoveMarker(marker);
+        }
+    }
+    
+    private Vector2Int GetTeamPosition()
+    {
+        if (teamPlacement != null)
+        {
+            return teamPlacement.GetCurrentTeamPosition();
+        }
+        return Vector2Int.zero;
+    }
+    
+    private float GetTeamPerceptionRange()
+    {
+        if (perceptionVisualizer != null)
+        {
+            // Access the perception range from the visualizer
+            // We need to get the team's highest perception and apply the multiplier
+            return GetHighestTeamPerception() * 1.0f; // Use same multiplier as perception visualizer
+        }
+        return 0f;
+    }
+    
+    private int GetHighestTeamPerception()
+    {
+        // This mirrors the logic from TeamPerceptionVisualizer
+        int maxPerception = 0;
+        List<TeamCharacter> teamMembers = GetCurrentTeamMembers();
+        
+        if (teamMembers == null || teamMembers.Count == 0)
+            return 0;
+        
+        foreach (TeamCharacter character in teamMembers)
+        {
+            if (character != null)
+            {
+                int characterPerception = character.FinalPerception;
+                if (characterPerception > maxPerception)
+                {
+                    maxPerception = characterPerception;
+                }
+            }
+        }
+        
+        return maxPerception;
+    }
+    
+    private List<TeamCharacter> GetCurrentTeamMembers()
+    {
+        // Use the same logic as TeamPerceptionVisualizer
+        var teamSelectScript = FindFirstObjectByType<MainTeamSelectScript>();
+        var gameStateManager = FindFirstObjectByType<GameStateManager>();
+        
+        // Try to get team from MainTeamSelectScript first
+        if (teamSelectScript != null)
+        {
+            var configuredCharacters = teamSelectScript.GetConfiguredCharacters();
+            if (configuredCharacters != null && configuredCharacters.Count > 0)
+            {
+                return configuredCharacters;
+            }
+        }
+        
+        // Fall back to GameStateManager
+        if (gameStateManager != null && gameStateManager.savedTeam != null)
+        {
+            var savedTeamList = new List<TeamCharacter>();
+            foreach (var character in gameStateManager.savedTeam)
+            {
+                if (character != null)
+                    savedTeamList.Add(character);
+            }
+            
+            if (savedTeamList.Count > 0)
+            {
+                return savedTeamList;
+            }
+        }
+        
+        // Final fallback: Load from PlayerPrefs
+        var playerPrefTeam = new List<TeamCharacter>();
+        for (int i = 0; i < 4; i++)
+        {
+            string prefix = $"Character{i}_";
+            if (PlayerPrefs.HasKey(prefix + "Exists") && PlayerPrefs.GetInt(prefix + "Exists") == 1)
+            {
+                var character = new TeamCharacter();
+                character.name = PlayerPrefs.GetString(prefix + "Name", "");
+                character.isMale = PlayerPrefs.GetInt(prefix + "IsMale", 1) == 1;
+                character.strength = PlayerPrefs.GetInt(prefix + "Strength", 10);
+                character.dexterity = PlayerPrefs.GetInt(prefix + "Dexterity", 10);
+                character.constitution = PlayerPrefs.GetInt(prefix + "Constitution", 10);
+                character.wisdom = PlayerPrefs.GetInt(prefix + "Wisdom", 10);
+                character.perception = PlayerPrefs.GetInt(prefix + "Perception", 10);
+                character.gold = PlayerPrefs.GetInt(prefix + "Gold", 25);
+                
+                playerPrefTeam.Add(character);
+            }
+        }
+        
+        return playerPrefTeam.Count > 0 ? playerPrefTeam : null;
+    }
+    
+    private Vector2Int FindValidMarkerPosition(Vector2Int teamPosition, float perceptionRange)
+    {
+        int maxAttempts = 20;
+        
+        for (int i = 0; i < maxAttempts; i++)
+        {
+            // Generate random position within perception range
+            float angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
+            float distance = Random.Range(minDistanceFromTeam, Mathf.Min(maxDistanceFromTeam, perceptionRange));
+            
+            Vector2Int candidatePosition = new Vector2Int(
+                teamPosition.x + Mathf.RoundToInt(Mathf.Cos(angle) * distance),
+                teamPosition.y + Mathf.RoundToInt(Mathf.Sin(angle) * distance)
+            );
+            
+            // Check if position is valid
+            if (IsValidMarkerPosition(candidatePosition))
+            {
+                return candidatePosition;
+            }
+        }
+        
+        return Vector2Int.zero; // Failed to find valid position
+    }
+    
+    private bool IsValidMarkerPosition(Vector2Int position)
+    {
+        // Check if position is already occupied
+        if (occupiedPositions.Contains(position))
+        {
+            return false;
+        }
+        
+        // Check if position is valid on the map
+        if (mapMaker != null && mapMaker.GetMapData() != null)
+        {
+            return mapMaker.GetMapData().IsValidPosition(position.x, position.y);
+        }
+        
+        return true; // Default to valid if no map check available
+    }
+    
+    private GameObject CreateMarkerObject(Vector2Int position)
+    {
+        GameObject markerObject;
+        
+        if (eventMarkerPrefab != null)
+        {
+            markerObject = Instantiate(eventMarkerPrefab);
+        }
+        else
+        {
+            // Create simple sphere marker
+            markerObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
+            markerObject.transform.localScale = Vector3.one * markerSize;
+            
+            // Remove collider to avoid interference
+            var collider = markerObject.GetComponent<Collider>();
+            if (collider != null)
+            {
+                DestroyImmediate(collider);
+            }
+        }
+        
+        // Position the marker
+        Vector3 worldPosition = GetWorldPosition(position);
+        markerObject.transform.position = worldPosition;
+        
+        // Apply material and color
+        ApplyMarkerMaterial(markerObject);
+        
+        // Set name and tag
+        markerObject.name = "EventMarker";
+        // Use default "Untagged" tag to avoid tag definition errors
+        markerObject.tag = "Untagged";
+        
+        return markerObject;
+    }
+    
+    private Vector3 GetWorldPosition(Vector2Int mapPosition)
+    {
+        // Convert map coordinates to world position
+        float tileSize = 1f;
+        if (mapMaker?.mapVisualizer != null)
+        {
+            tileSize = mapMaker.mapVisualizer.tileSize;
+        }
+        
+        return new Vector3(
+            mapPosition.x * tileSize,
+            markerHeight,
+            mapPosition.y * tileSize
+        );
+    }
+    
+    private void ApplyMarkerMaterial(GameObject markerObject)
+    {
+        Renderer renderer = markerObject.GetComponent<Renderer>();
+        if (renderer == null) return;
+        
+        if (eventMarkerMaterial != null)
+        {
+            renderer.material = eventMarkerMaterial;
+        }
+        else
+        {
+            // Create a simple red material
+            Material material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
+            if (material.shader == null)
+            {
+                material = new Material(Shader.Find("Standard"));
+            }
+            
+            material.color = eventMarkerColor;
+            
+            // Add emission for visibility
+            if (material.HasProperty("_EmissionColor"))
+            {
+                material.EnableKeyword("_EMISSION");
+                material.SetColor("_EmissionColor", eventMarkerColor * 0.5f);
+            }
+            
+            renderer.material = material;
+        }
+    }
+    
+    private void CleanupExpiredMarkers()
+    {
+        var markersToRemove = activeMarkers.Where(m => 
+            !m.isActive || 
+            m.gameObject == null || 
+            (Time.time - m.spawnTime) > markerLifetime
+        ).ToList();
+        
+        foreach (var marker in markersToRemove)
+        {
+            RemoveMarker(marker);
+        }
+    }
+    
+    private void UpdateMarkerPulsing()
+    {
+        float pulseValue = (Mathf.Sin(Time.time * pulseSpeed) + 1f) * 0.5f; // 0 to 1
+        float scale = 1f + (pulseValue * pulseIntensity);
+        
+        foreach (var marker in activeMarkers)
+        {
+            if (marker.isActive && marker.gameObject != null)
+            {
+                marker.gameObject.transform.localScale = Vector3.one * (markerSize * scale);
+            }
+        }
+    }
+    
+    private void RemoveMarker(EventMarker marker)
+    {
+        if (marker.gameObject != null)
+        {
+            DestroyImmediate(marker.gameObject);
+        }
+        
+        activeMarkers.Remove(marker);
+        occupiedPositions.Remove(marker.position);
+        marker.isActive = false;
+    }
+    
+    /// <summary>
+    /// Gets the number of currently active event markers
+    /// </summary>
+    public int GetActiveMarkerCount()
+    {
+        return activeMarkers.Count(m => m.isActive);
+    }
+    
+    /// <summary>
+    /// Debug method to show all active markers
+    /// </summary>
+    [ContextMenu("Show Active Markers")]
+    public void ShowActiveMarkers()
+    {
+        Debug.Log($"=== Active Event Markers ({activeMarkers.Count}) ===");
+        foreach (var marker in activeMarkers)
+        {
+            if (marker.isActive)
+            {
+                float age = Time.time - marker.spawnTime;
+                Debug.Log($"- {marker.travelEvent.eventName} at {marker.position} (age: {age:F1}s)");
+            }
+        }
+    }
+}
+
+/// <summary>
+/// Data structure for tracking active event markers
+/// </summary>
+[System.Serializable]
+public class EventMarker
+{
+    public GameObject gameObject;
+    public TravelEvent travelEvent;
+    public Vector2Int position;
+    public float spawnTime;
+    public bool isActive;
+}

+ 120 - 0
PERCEPTION_EVENT_SYSTEM_SUMMARY.md

@@ -0,0 +1,120 @@
+# Perception and Spottable Event System - Implementation Summary
+
+## 🎯 What Was Implemented
+
+Your RPG now has a **perception-based spottable event system** where events can appear as red markers within your team's perception range, allowing players to choose whether to approach them or avoid them.
+
+## 📁 New Files Created
+
+### **Core System Files**
+1. **`EventTriggerType.cs`** - Enum defining Automatic vs Spottable events
+2. **`EventMarkerVisualizer.cs`** - Manages red event markers within perception range
+3. **`SpottableSkeletonPatrol.cs`** - Example spottable combat event
+4. **`PerceptionEventSystemSetup.cs`** - Auto-setup script for the entire system
+
+### **Documentation**
+5. **`PERCEPTION_SPOTTABLE_EVENTS_GUIDE.md`** - Complete implementation guide
+
+## 🔧 Modified Files
+
+### **Enhanced Existing Systems**
+1. **`TravelEvent.cs`** - Added `EventTriggerType` enum and `triggerType` field
+2. **`TravelEventSystem.cs`** - Enhanced to handle both automatic and spottable events
+
+## 🎮 How It Works
+
+### **Automatic Events (Original Behavior)**
+- Events trigger immediately when conditions are met
+- No change to existing functionality
+- Set `triggerType = EventTriggerType.Automatic`
+
+### **Spottable Events (New Feature)**
+- Events appear as **red markers** within perception range
+- Players can approach markers to trigger events
+- Players can avoid markers to skip events
+- Set `triggerType = EventTriggerType.Spottable`
+
+### **Perception Integration**
+- **Purple circle** shows team's perception range
+- **Red dots** spawn within perception range for spottable events
+- Higher perception = larger detection range = more strategic options
+
+## 🚀 Quick Start Instructions
+
+### **Step 1: Setup**
+1. In your travel scene, add `PerceptionEventSystemSetup` component to any GameObject
+2. Click **"Setup Perception Event System"** in inspector
+3. Use **"Show System Status"** to verify all components are active
+
+### **Step 2: Test**
+1. Play your travel scene
+2. Look for purple perception circle around team marker
+3. Travel around and watch for red event markers
+4. Move team near red markers to trigger events
+
+### **Step 3: Create Events**
+- Right-click in Project → Create → RPG → Travel Events → Spottable → Skeleton Patrol
+- Or modify existing events by changing `Trigger Type` to `Spottable`
+
+## 🎯 Key Benefits
+
+### **Strategic Gameplay**
+- **Player Choice**: Approach or avoid encounters
+- **Perception Value**: High perception = see threats coming
+- **Risk Management**: Engage only when ready
+
+### **Enhanced Immersion**
+- **Visual Feedback**: See your awareness range
+- **Realistic Scouting**: Spot enemies before they spot you
+- **Environmental Storytelling**: Marker density shows area danger
+
+### **Tactical Depth**
+- **Route Planning**: Navigate around dangerous areas
+- **Resource Management**: Choose battles wisely
+- **Character Building**: Perception becomes strategically valuable
+
+## 🔄 Backward Compatibility
+
+### **Existing Events Still Work**
+- All current events remain `Automatic` by default
+- No change to existing gameplay
+- System is additive, not replacing
+
+### **Gradual Migration**
+- Can convert events one by one to `Spottable`
+- Mix of automatic and spottable events works perfectly
+- Test with one spottable event first
+
+## 🎮 Player Experience
+
+### **Before**
+1. Team travels
+2. Random event interrupts travel
+3. Must deal with event immediately
+
+### **After**
+1. Team travels with visible perception range
+2. Red markers appear for potential events
+3. Player sees opportunities/threats coming
+4. Player chooses to engage or avoid
+5. Perception becomes strategically valuable
+
+## 🔧 System Components Summary
+
+1. **EventTriggerType** - Enum for event trigger behavior
+2. **EventMarkerVisualizer** - Red marker management
+3. **TeamPerceptionVisualizer** - Purple perception circle (enhanced)
+4. **TravelEventSystem** - Event spawning and triggering (enhanced)
+5. **PerceptionEventSystemSetup** - One-click setup tool
+
+## ✅ System Status Check
+
+Use the setup component's **"Show System Status"** to verify:
+- ✅ TeamPerceptionVisualizer: Purple circle around team
+- ✅ EventMarkerVisualizer: Red marker management
+- ✅ TravelEventSystem: Event spawning integration
+- ✅ SimpleTeamPlacement: Team position tracking
+
+## 🎉 Ready to Use!
+
+Your enhanced perception and event system is now ready! Players can spot events as red markers within their perception range and choose whether to approach them, making perception a valuable strategic stat and giving players meaningful choices during travel.

+ 184 - 0
PERCEPTION_SPOTTABLE_EVENTS_GUIDE.md

@@ -0,0 +1,184 @@
+# Perception-Based Spottable Event System - Implementation Guide
+
+## Overview
+
+Your RPG now supports **spottable events** that appear as red markers within your team's perception range! Instead of all events triggering automatically, players can now spot potential encounters and choose whether to approach them or avoid them.
+
+## 🎯 Key Features
+
+### **Spottable Event Markers**
+- **Red dots** appear within your team's perception circle
+- Markers represent events that can be spotted before they trigger
+- Players can choose to approach markers to trigger events
+- Markers disappear after 30 seconds if not approached
+
+### **Enhanced Perception System**
+- Team's perception range (purple circle) now serves a gameplay purpose
+- Higher perception = larger detection range for events
+- Events can only be spotted within the perception radius
+
+### **Dual Event System**
+- **Automatic Events**: Trigger immediately (legacy behavior)
+- **Spottable Events**: Appear as markers first, trigger when approached
+
+## 🚀 Quick Setup
+
+### **Step 1: Add Setup Component**
+1. In your scene with the travel system, find any GameObject (or create a new one)
+2. Add the `PerceptionEventSystemSetup` component
+3. Click **"Setup Perception Event System"** in the inspector
+4. The system will automatically add required components
+
+### **Step 2: Verify Integration**
+- Use the **"Show System Status"** button to verify all components are present
+- You should see ✅ for all required components
+
+### **Step 3: Test the System**
+1. Start your travel scene
+2. You should see the purple perception circle around your team marker
+3. As you travel, red event markers may appear within the circle
+4. Move your team near a red marker to trigger the event
+
+## 📋 Creating Spottable Events
+
+### **Option 1: Use Pre-built Events**
+- **SpottableSkeletonPatrol**: Skeleton warriors that can be spotted and avoided
+- Right-click in Project → Create → RPG → Travel Events → Spottable → Skeleton Patrol
+
+### **Option 2: Convert Existing Events**
+1. Open any existing `TravelEvent` asset
+2. Change **Trigger Type** from `Automatic` to `Spottable`
+3. The event will now appear as a red marker instead of triggering immediately
+
+### **Option 3: Create Custom Spottable Events**
+```csharp
+[CreateAssetMenu(fileName = "MySpottableEvent", menuName = "RPG/Travel Events/Spottable/Custom Event")]
+public class MySpottableEvent : CombatTravelEvent
+{
+    void OnEnable()
+    {
+        triggerType = EventTriggerType.Spottable;  // Key setting!
+        eventName = "My Spottable Event";
+        // Configure other settings...
+    }
+}
+```
+
+## ⚙️ System Components
+
+### **EventMarkerVisualizer**
+- Manages red event markers within perception range
+- Handles marker spawning, lifetime, and cleanup
+- Configurable marker appearance and behavior
+
+### **TeamPerceptionVisualizer** (Enhanced)
+- Shows purple perception circle around team
+- Now used by event system to determine marker spawn range
+- Based on team's highest perception value
+
+### **TravelEventSystem** (Enhanced)
+- Supports both automatic and spottable events
+- Checks for marker triggers when team moves
+- Spawns markers for spottable events
+
+### **EventTriggerType** (New Enum)
+- `Automatic`: Traditional immediate triggering
+- `Spottable`: Appears as marker first, triggers when approached
+
+## 🎮 Player Experience
+
+### **Before (Automatic Events)**
+1. Team travels
+2. Event triggers randomly
+3. Player must deal with event immediately
+
+### **After (Spottable Events)**
+1. Team travels with visible perception range
+2. Red markers appear for potential events
+3. Player can see threats/opportunities coming
+4. Player chooses to approach or avoid
+5. Strategic perception building becomes valuable
+
+## 🔧 Configuration Options
+
+### **EventMarkerVisualizer Settings**
+- **Max Active Markers**: How many red dots can be on screen (default: 5)
+- **Marker Lifetime**: How long markers last if not approached (default: 30s)
+- **Min/Max Distance**: How close/far from team markers can spawn
+- **Marker Color**: Red by default, customizable
+- **Pulsing Effect**: Visual pulsing to draw attention
+
+### **Event Settings**
+- **Trigger Type**: Choose Automatic or Spottable per event
+- **Terrain Preferences**: Where events are more/less likely
+- **Rarity**: How often events spawn
+
+### **Perception Settings**
+- **Perception Multiplier**: How perception value converts to world distance
+- **Visualization**: Purple circle can be toggled on/off
+
+## 📊 Benefits of the New System
+
+### **Strategic Gameplay**
+- **Perception becomes valuable**: High perception = see threats/opportunities sooner
+- **Player agency**: Choose which encounters to face
+- **Risk management**: Avoid dangerous encounters when weakened
+
+### **Improved Immersion**
+- **Visual feedback**: See your party's awareness range
+- **Realistic encounters**: Spot enemies before they spot you
+- **Environmental storytelling**: Markers show the danger level of areas
+
+### **Tactical Depth**
+- **Scouting**: High perception characters become scouts
+- **Route planning**: Navigate around dangerous encounters
+- **Resource management**: Engage only when ready
+
+## 🐛 Troubleshooting
+
+### **No Red Markers Appearing**
+- Check that events have `triggerType = EventTriggerType.Spottable`
+- Verify EventMarkerVisualizer is in the scene
+- Ensure team has perception > 0
+- Check event rarity settings
+
+### **Purple Circle Not Showing**
+- Check that TeamPerceptionVisualizer is in the scene
+- Verify team members have perception values
+- Check visualization is enabled in PlayerPrefs
+
+### **Events Not Triggering When Approached**
+- Verify team position is within trigger distance (1.5 units)
+- Check TravelEventSystem integration
+- Look for error messages in console
+
+## 🎯 Advanced Usage
+
+### **Creating Event Chains**
+Create multiple spottable events that spawn based on player choices:
+```csharp
+// First event spawns marker for "Bandit Camp"
+// If approached, can spawn "Bandit Leader" marker
+// If avoided, can spawn "Bandit Patrol" marker later
+```
+
+### **Perception-Based Difficulty**
+Adjust event spawning based on team perception:
+```csharp
+// Higher perception = spot better opportunities
+// Lower perception = more dangerous surprises
+```
+
+### **Environmental Storytelling**
+Use marker density to show area danger:
+```csharp
+// Forests: Many spottable encounters
+// Roads: Fewer spottable, more automatic (safe travel)
+// Mountains: Very dangerous spottable encounters
+```
+
+## 🎉 Conclusion
+
+The perception-based spottable event system transforms travel from random interruptions into strategic exploration. Players now have meaningful choices, perception becomes a valuable stat, and the game world feels more immersive and tactical.
+
+Your players can now truly scout ahead, avoid dangerous encounters when needed, and make informed decisions about which opportunities to pursue!