using Core; using Minigames.TrashMaze.Core; using UnityEngine; using System.Collections; using AppleHills.Core.Settings; namespace Minigames.TrashMaze.Objects { /// /// Reveal mode for object visibility /// public enum RevealMode { Binary, // Simple on/off reveal (current system) Progressive // Pixel-by-pixel progressive reveal with stamp texture } /// /// Component for objects that need reveal memory (obstacles, booster packs, treasures). /// Tracks if object has been revealed and updates material properties accordingly. /// [RequireComponent(typeof(SpriteRenderer))] public class RevealableObject : MonoBehaviour { [Header("Reveal Settings")] [SerializeField] private RevealMode revealMode = RevealMode.Binary; [Header("Textures")] [SerializeField] private Sprite normalSprite; [SerializeField] private Sprite outlineSprite; [Header("Progressive Reveal Settings")] [SerializeField, Range(0.5f, 3f)] private float stampWorldRadius = 1.5f; [Tooltip("Size of each stamp in world units. Smaller = more gradual reveal. Should be smaller than vision radius.")] [Header("Object Type")] [SerializeField] private bool isBoosterPack = false; [SerializeField] private bool isExit = false; private SpriteRenderer _spriteRenderer; private Material _instanceMaterial; private bool _hasBeenRevealed = false; private bool _isCollected = false; // Material property IDs (cached for performance) private static readonly int IsRevealedID = Shader.PropertyToID("_IsRevealed"); private static readonly int IsInVisionID = Shader.PropertyToID("_IsInVision"); private static readonly int RevealMaskID = Shader.PropertyToID("_RevealMask"); // Progressive reveal system private RenderTexture _revealStampTexture; private Coroutine _stampCoroutine; private Bounds _objectBounds; private float _activationDistance; // Binary reveal system private Coroutine _binaryRevealCoroutine; private void Awake() { _spriteRenderer = GetComponent(); if (_spriteRenderer.material == null) { Logging.Error($"[RevealableObject] No material assigned to {gameObject.name}"); return; } // Create instance material _instanceMaterial = new Material(_spriteRenderer.material); _spriteRenderer.material = _instanceMaterial; // Call mode-specific initialization - COMPLETELY SEPARATE if (revealMode == RevealMode.Binary) { InitializeBinaryMode(); } else if (revealMode == RevealMode.Progressive) { InitializeProgressiveMode(); } } // ======================================== // BINARY MODE INITIALIZATION // ======================================== private void InitializeBinaryMode() { // Validate Binary shader string shaderName = _instanceMaterial.shader.name; if (!shaderName.Contains("ObjectVisibility") || shaderName.Contains("Progressive")) { Logging.Error($"[RevealableObject] {gameObject.name} Binary mode needs shader 'TrashMaze/ObjectVisibility', currently: {shaderName}"); } // Set initial Binary mode properties _instanceMaterial.SetFloat(IsRevealedID, 0f); _instanceMaterial.SetFloat(IsInVisionID, 0f); // Set textures if (normalSprite != null) { _instanceMaterial.SetTexture("_MainTex", normalSprite.texture); } if (outlineSprite != null) { _instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture); } Logging.Debug($"[RevealableObject] {gameObject.name} Binary mode initialized"); } // ======================================== // PROGRESSIVE MODE INITIALIZATION // ======================================== private void InitializeProgressiveMode() { // Validate Progressive shader string shaderName = _instanceMaterial.shader.name; if (!shaderName.Contains("ObjectVisibilityProgressive")) { Logging.Error($"[RevealableObject] {gameObject.name} Progressive mode needs shader 'TrashMaze/ObjectVisibilityProgressive', currently: {shaderName}"); } // Initialize progressive reveal system InitializeProgressiveReveal(); } /// /// Initialize progressive reveal system with dynamic texture sizing /// private void InitializeProgressiveReveal() { // Get object bounds for UV calculations _objectBounds = _spriteRenderer.bounds; // Load activation distance from settings (use vision radius from follower settings) var configs = GameManager.GetSettingsObject(); _activationDistance = configs.FollowerMovement.TrashMazeVisionRadius; Logging.Debug($"[RevealableObject] {gameObject.name} loaded activation distance from settings: {_activationDistance}"); // Dynamically determine texture size from sprite int textureWidth = 128; int textureHeight = 128; if (normalSprite != null && normalSprite.texture != null) { // Use sprite's texture resolution (match 1:1 for pixel-perfect) textureWidth = normalSprite.texture.width; textureHeight = normalSprite.texture.height; } else { Logging.Warning($"[RevealableObject] {gameObject.name} in Progressive mode but normalSprite not assigned! Using default 128x128 texture. Assign Normal Sprite in inspector!"); } // Create reveal stamp texture (R8 format = 8-bit grayscale, minimal memory) _revealStampTexture = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.R8); _revealStampTexture.filterMode = FilterMode.Point; // Sharp edges for binary reveals _revealStampTexture.wrapMode = TextureWrapMode.Clamp; _revealStampTexture.Create(); // Explicitly create the texture // Clear to black (nothing revealed initially) RenderTexture previousActive = RenderTexture.active; RenderTexture.active = _revealStampTexture; GL.Clear(false, true, Color.black); RenderTexture.active = previousActive; Logging.Debug($"[RevealableObject] {gameObject.name} cleared reveal texture to black - should be invisible initially"); // Set Progressive shader properties _instanceMaterial.SetTexture(RevealMaskID, _revealStampTexture); // Progressive shader uses global _PlayerWorldPos and _VisionRadius (set by PulverController) // No need to set _IsInVision - shader does per-pixel distance checks // Set textures if (normalSprite != null) { _instanceMaterial.SetTexture("_MainTex", normalSprite.texture); } if (outlineSprite != null) { _instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture); } Logging.Debug($"[RevealableObject] {gameObject.name} Progressive mode initialized: {textureWidth}x{textureHeight} reveal texture"); } private void Start() { if (PulverController.Instance == null) { Logging.Error($"[RevealableObject] {gameObject.name} cannot start - PulverController not found!"); return; } // Start mode-specific runtime logic - COMPLETELY SEPARATE if (revealMode == RevealMode.Binary) { StartBinaryModeTracking(); } else if (revealMode == RevealMode.Progressive) { StartProgressiveModeTracking(); } } // ======================================== // BINARY MODE: Start tracking // ======================================== private void StartBinaryModeTracking() { _binaryRevealCoroutine = StartCoroutine(BinaryRevealTrackingCoroutine()); Logging.Debug($"[RevealableObject] {gameObject.name} Binary mode tracking started"); } // ======================================== // PROGRESSIVE MODE: Start tracking // ======================================== private void StartProgressiveModeTracking() { // Subscribe to movement events for stamping PulverController.Instance.OnMovementStarted += OnPlayerMovementStarted; PulverController.Instance.OnMovementStopped += OnPlayerMovementStopped; // NO vision tracking coroutine - Progressive shader does per-pixel distance checks using global _PlayerWorldPos Logging.Debug($"[RevealableObject] {gameObject.name} Progressive mode tracking started"); } // ======================================== // BINARY MODE: Vision-based reveal coroutine // ======================================== /// /// Binary mode coroutine - tracks player distance and updates vision/reveal flags /// Runs continuously, checks every 0.1s for performance /// private IEnumerator BinaryRevealTrackingCoroutine() { while (!_isCollected && _instanceMaterial != null) { // Calculate distance to player float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition); bool isInRadius = distance < PulverController.VisionRadius; // Set real-time vision flag (controls shader color vs outline) _instanceMaterial.SetFloat(IsInVisionID, isInRadius ? 1f : 0f); // Set reveal flag (once revealed, stays revealed) if (isInRadius && !_hasBeenRevealed) { _hasBeenRevealed = true; _instanceMaterial.SetFloat(IsRevealedID, 1f); Logging.Debug($"[RevealableObject] {gameObject.name} revealed!"); } // Wait before next check (reduces CPU load) yield return new WaitForSeconds(0.1f); } } // ======================================== // PROGRESSIVE MODE: Event-based stamp reveal // ======================================== /// /// Called when player starts moving - begin stamping if near object /// private void OnPlayerMovementStarted() { if (_isCollected || revealMode != RevealMode.Progressive) return; // Check if player's vision circle could overlap with object bounds // Use closest point on bounds to check distance Vector2 playerPos = PulverController.PlayerPosition; Vector2 closestPoint = _objectBounds.ClosestPoint(playerPos); float distanceToBounds = Vector2.Distance(playerPos, closestPoint); // Start stamping if vision radius could reach the object // Add padding to account for vision radius overlap float activationThreshold = _activationDistance + _objectBounds.extents.magnitude; if (distanceToBounds < activationThreshold && _stampCoroutine == null) { _stampCoroutine = StartCoroutine(StampRevealCoroutine()); Logging.Debug($"[RevealableObject] {gameObject.name} started stamping coroutine (distanceToBounds: {distanceToBounds:F2}, threshold: {activationThreshold:F2})"); } } /// /// Called when player stops moving - stop stamping /// private void OnPlayerMovementStopped() { if (_stampCoroutine != null) { StopCoroutine(_stampCoroutine); _stampCoroutine = null; } } /// /// Coroutine that stamps reveal texture while player is moving and near object /// private IEnumerator StampRevealCoroutine() { while (!_isCollected) { // Check if player's vision circle overlaps with object bounds Vector2 playerPos = PulverController.PlayerPosition; Vector2 closestPoint = _objectBounds.ClosestPoint(playerPos); float distanceToBounds = Vector2.Distance(playerPos, closestPoint); // Calculate activation threshold with padding float activationThreshold = _activationDistance + _objectBounds.extents.magnitude; // If player moved too far away, stop stamping if (distanceToBounds >= activationThreshold) { Logging.Debug($"[RevealableObject] {gameObject.name} stopping stamping coroutine (too far)"); _stampCoroutine = null; yield break; } // Stamp if player's vision radius reaches any part of the object if (distanceToBounds < PulverController.VisionRadius) { StampPlayerPosition(); } // Wait before next stamp (reduces GPU writes) yield return new WaitForSeconds(0.1f); } } /// /// Stamp the player's current position onto the reveal texture /// Direct CPU-based stamping - calculates circle-rectangle intersection in world space /// private void StampPlayerPosition() { if (_revealStampTexture == null) { Logging.Warning($"[RevealableObject] {gameObject.name} cannot stamp: texture is null"); return; } // Get player position and vision radius in world space Vector2 playerWorldPos = PulverController.PlayerPosition; float visionRadius = PulverController.VisionRadius; // Get object bounds in world space Vector2 boundsMin = _objectBounds.min; Vector2 boundsSize = _objectBounds.size; // Get texture dimensions int texWidth = _revealStampTexture.width; int texHeight = _revealStampTexture.height; // Calculate the bounding box of the circle in world space Vector2 circleMin = playerWorldPos - Vector2.one * visionRadius; Vector2 circleMax = playerWorldPos + Vector2.one * visionRadius; // Calculate intersection of circle bounding box with object bounds Vector2 intersectMin = new Vector2( Mathf.Max(circleMin.x, boundsMin.x), Mathf.Max(circleMin.y, boundsMin.y) ); Vector2 intersectMax = new Vector2( Mathf.Min(circleMax.x, boundsMin.x + boundsSize.x), Mathf.Min(circleMax.y, boundsMin.y + boundsSize.y) ); // Check if there's any intersection if (intersectMin.x >= intersectMax.x || intersectMin.y >= intersectMax.y) { return; // No intersection, nothing to stamp } // Convert world space intersection to texture pixel coordinates int pixelMinX = Mathf.FloorToInt((intersectMin.x - boundsMin.x) / boundsSize.x * texWidth); int pixelMaxX = Mathf.CeilToInt((intersectMax.x - boundsMin.x) / boundsSize.x * texWidth); int pixelMinY = Mathf.FloorToInt((intersectMin.y - boundsMin.y) / boundsSize.y * texHeight); int pixelMaxY = Mathf.CeilToInt((intersectMax.y - boundsMin.y) / boundsSize.y * texHeight); // Clamp to texture bounds pixelMinX = Mathf.Max(0, pixelMinX); pixelMaxX = Mathf.Min(texWidth, pixelMaxX); pixelMinY = Mathf.Max(0, pixelMinY); pixelMaxY = Mathf.Min(texHeight, pixelMaxY); // Read current texture data RenderTexture.active = _revealStampTexture; Texture2D tempTex = new Texture2D(texWidth, texHeight, TextureFormat.R8, false); tempTex.ReadPixels(new Rect(0, 0, texWidth, texHeight), 0, 0); tempTex.Apply(); // Stamp pixels within the circle bool anyPixelStamped = false; float radiusSquared = visionRadius * visionRadius; for (int py = pixelMinY; py < pixelMaxY; py++) { for (int px = pixelMinX; px < pixelMaxX; px++) { // Convert pixel coordinates back to world space float worldX = boundsMin.x + (px / (float)texWidth) * boundsSize.x; float worldY = boundsMin.y + (py / (float)texHeight) * boundsSize.y; // Check if this pixel is within the circle float dx = worldX - playerWorldPos.x; float dy = worldY - playerWorldPos.y; float distSquared = dx * dx + dy * dy; if (distSquared <= radiusSquared) { // Stamp this pixel (set to white) tempTex.SetPixel(px, py, Color.white); anyPixelStamped = true; } } } if (anyPixelStamped) { // Upload modified texture back to GPU tempTex.Apply(); Graphics.CopyTexture(tempTex, _revealStampTexture); Logging.Debug($"[RevealableObject] {gameObject.name} stamped pixels at world pos ({playerWorldPos.x:F2}, {playerWorldPos.y:F2}), radius {visionRadius:F2}"); } RenderTexture.active = null; Destroy(tempTex); } private void OnTriggerEnter2D(Collider2D other) { // Check if player is interacting if (other.CompareTag("Player") && _hasBeenRevealed && !_isCollected) { HandleInteraction(); } } private void HandleInteraction() { if (isBoosterPack) { CollectBoosterPack(); } else if (isExit) { ActivateExit(); } } private void CollectBoosterPack() { _isCollected = true; Logging.Debug($"[RevealableObject] Booster pack collected: {gameObject.name}"); // Notify controller if (TrashMazeController.Instance != null) { TrashMazeController.Instance.OnBoosterPackCollected(); } // Destroy object Destroy(gameObject); } private void ActivateExit() { Logging.Debug($"[RevealableObject] Exit activated: {gameObject.name}"); // Notify controller if (TrashMazeController.Instance != null) { TrashMazeController.Instance.OnExitReached(); } } private void OnDestroy() { // Stop Binary mode coroutine if (_binaryRevealCoroutine != null) { StopCoroutine(_binaryRevealCoroutine); } // Unsubscribe from Progressive mode movement events if (revealMode == RevealMode.Progressive && PulverController.Instance != null) { PulverController.Instance.OnMovementStarted -= OnPlayerMovementStarted; PulverController.Instance.OnMovementStopped -= OnPlayerMovementStopped; } // Stop Progressive mode stamping coroutine if (_stampCoroutine != null) { StopCoroutine(_stampCoroutine); } // Clean up progressive reveal resources if (_revealStampTexture != null) { _revealStampTexture.Release(); Destroy(_revealStampTexture); } // Clean up instance material if (_instanceMaterial != null) { Destroy(_instanceMaterial); } } /// /// Check if object is currently visible to player /// public bool IsVisible() { float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition); return distance < PulverController.VisionRadius; } /// /// Check if object has been revealed at any point /// public bool HasBeenRevealed() { return _hasBeenRevealed; } /// /// Force reveal the object (for debugging or special cases) /// public void ForceReveal() { _hasBeenRevealed = true; if (_instanceMaterial != null) { _instanceMaterial.SetFloat(IsRevealedID, 1f); } } } }