2025-12-10 11:14:10 +00:00
using Core ;
using Minigames.TrashMaze.Core ;
using UnityEngine ;
using System.Collections ;
using AppleHills.Core.Settings ;
2025-12-15 11:59:40 +01:00
using Core.Settings ;
2025-12-10 11:14:10 +00:00
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Reveal mode for object visibility
/// </summary>
public enum RevealMode
{
Binary , // Simple on/off reveal (current system)
Progressive // Pixel-by-pixel progressive reveal with stamp texture
}
/// <summary>
/// Component for objects that need reveal memory (obstacles, booster packs, treasures).
/// Tracks if object has been revealed and updates material properties accordingly.
/// </summary>
[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 < SpriteRenderer > ( ) ;
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 ( ) ;
}
/// <summary>
/// Initialize progressive reveal system with dynamic texture sizing
/// </summary>
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 < IPlayerMovementConfigs > ( ) ;
_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
// ========================================
/// <summary>
/// Binary mode coroutine - tracks player distance and updates vision/reveal flags
/// Runs continuously, checks every 0.1s for performance
/// </summary>
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
// ========================================
/// <summary>
/// Called when player starts moving - begin stamping if near object
/// </summary>
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})" ) ;
}
}
/// <summary>
/// Called when player stops moving - stop stamping
/// </summary>
private void OnPlayerMovementStopped ( )
{
if ( _stampCoroutine ! = null )
{
StopCoroutine ( _stampCoroutine ) ;
_stampCoroutine = null ;
}
}
/// <summary>
/// Coroutine that stamps reveal texture while player is moving and near object
/// </summary>
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 ) ;
}
}
/// <summary>
/// Stamp the player's current position onto the reveal texture
/// Direct CPU-based stamping - calculates circle-rectangle intersection in world space
/// </summary>
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 ) ;
}
}
/// <summary>
/// Check if object is currently visible to player
/// </summary>
public bool IsVisible ( )
{
float distance = Vector2 . Distance ( transform . position , PulverController . PlayerPosition ) ;
return distance < PulverController . VisionRadius ;
}
/// <summary>
/// Check if object has been revealed at any point
/// </summary>
public bool HasBeenRevealed ( )
{
return _hasBeenRevealed ;
}
/// <summary>
/// Force reveal the object (for debugging or special cases)
/// </summary>
public void ForceReveal ( )
{
_hasBeenRevealed = true ;
if ( _instanceMaterial ! = null )
{
_instanceMaterial . SetFloat ( IsRevealedID , 1f ) ;
}
}
}
}