206 lines
6.0 KiB
C#
206 lines
6.0 KiB
C#
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<Collider>();
|
|
animator = GetComponentInChildren<Animator>();
|
|
audioSource = GetComponent<AudioSource>();
|
|
|
|
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<NetworkObject>();
|
|
if (netObj != null)
|
|
{
|
|
var playerData = target.GetComponent<PlayerData>();
|
|
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<Renderer>();
|
|
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<Renderer>();
|
|
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);
|
|
}
|
|
}
|
|
}
|