using Fusion; using UnityEngine; public enum TrapState { Arming, // Newly placed, arming delay active Armed, // Active and scanning for targets Triggered, // Tripped by a target, applying effects Despawning // Mark for cleanup/destruction } [RequireComponent(typeof(NetworkObject))] public abstract class TrapBase : NetworkBehaviour { [Header("Trap Config")] [SerializeField] protected TrapDataSO trapData; [Networked] public PlayerRef Owner { get; set; } [Networked, OnChangedRender(nameof(OnStateChanged))] public TrapState State { get; set; } [Networked] protected TickTimer ArmingTimer { get; set; } [Networked] protected TickTimer LifetimeTimer { get; set; } protected Collider trapCollider; protected Animator animator; protected AudioSource audioSource; public override void Spawned() { trapCollider = GetComponent(); animator = GetComponentInChildren(); audioSource = GetComponent(); if (Object.HasStateAuthority) { State = TrapState.Arming; ArmingTimer = TickTimer.CreateFromSeconds(Runner, trapData.ArmingDelay); LifetimeTimer = TickTimer.CreateFromSeconds(Runner, trapData.Lifetime); } ConfigureVisibility(); } public override void FixedUpdateNetwork() { if (!Object.HasStateAuthority) return; // Count down the arming delay if (State == TrapState.Arming && ArmingTimer.Expired(Runner)) { State = TrapState.Armed; } // Auto-despawn when lifetime timer expires if (LifetimeTimer.Expired(Runner)) { DespawnTrap(); } } protected virtual void OnTriggerEnter(Collider other) { // Physics collision triggers are only processed on Server/Host (State Authority) if (!Object.HasStateAuthority) return; if (State != TrapState.Armed) return; if (IsTargetValid(other.gameObject)) { TriggerTrap(other.gameObject); } } protected bool IsTargetValid(GameObject target) { // Verify target has a NetworkObject and check role through PlayerData var netObj = target.GetComponent(); if (netObj != null) { var playerData = target.GetComponent(); if (playerData != null && playerData.PlayerRole == _Role.Seeker) { return true; } } return false; } protected void TriggerTrap(GameObject victim) { State = TrapState.Triggered; OnTriggered(victim); } // Abstract methods to be implemented by specific traps protected abstract void OnTriggered(GameObject victim); protected virtual void ConfigureVisibility() { var localPlayer = Runner.LocalPlayer; PlayerDataManager pdm = PlayerDataManager.Instance; if (pdm != null && pdm.TryGetPlayerMetaData(localPlayer, out var meta)) { if (meta.Role == _Role.Trapper) { // Trapper sees their traps clearly gameObject.layer = LayerMask.NameToLayer("Default"); SetTrapAlpha(0.8f); } else { // Seeker: place in stealth layer if exists, otherwise disable MeshRenderers as fallback int stealthLayer = LayerMask.NameToLayer("StealthTrap"); if (stealthLayer != -1) { SetLayerRecursively(gameObject, stealthLayer); } else { SetRenderersEnabled(false); } } } } private void SetLayerRecursively(GameObject obj, int newLayer) { if (obj == null) return; obj.layer = newLayer; foreach (Transform child in obj.transform) { SetLayerRecursively(child.gameObject, newLayer); } } private void SetTrapAlpha(float alpha) { Renderer[] renderers = GetComponentsInChildren(); foreach (var r in renderers) { foreach (var mat in r.materials) { Color col = mat.color; col.a = alpha; mat.color = col; } } } private void SetRenderersEnabled(bool isEnabled) { Renderer[] renderers = GetComponentsInChildren(); foreach (var r in renderers) { r.enabled = isEnabled; } } public void OnStateChanged() { HandleStateVisuals(); } protected virtual void HandleStateVisuals() { switch (State) { case TrapState.Arming: // Play placement sound effect or visual cue locally if (audioSource != null && trapData.PlaceSFX != null) { audioSource.PlayOneShot(trapData.PlaceSFX); } break; case TrapState.Armed: // Visual indicators of trap active state (e.g. flashing light) break; case TrapState.Triggered: // When triggered, reveal the trap mesh to everyone SetRenderersEnabled(true); if (animator != null) { animator.SetTrigger("Trigger"); } if (audioSource != null && trapData.TriggerSFX != null) { audioSource.PlayOneShot(trapData.TriggerSFX); } if (trapData.TriggerVFX != null) { Instantiate(trapData.TriggerVFX, transform.position, Quaternion.identity); } break; } } protected void DespawnTrap() { if (Object != null && Object.IsValid && Object.HasStateAuthority) { State = TrapState.Despawning; Runner.Despawn(Object); } } }