using Fusion; using UnityEngine; public class TrapPlacementController : NetworkBehaviour { [Header("Placement Setup")] [SerializeField] private TrapDataSO[] availableTraps; [SerializeField] private LayerMask placementLayerMask; private PlayerData playerData; private int selectedTrapIndex = 0; private GameObject currentGhostInstance; private bool isPreviewing = false; private float[] cooldownTimers; public override void Spawned() { playerData = GetComponent(); // Initialize cooldown tracking for each trap type if (availableTraps != null) { cooldownTimers = new float[availableTraps.Length]; } } void Update() { // Only run placement logic for the local player who is a Trapper if (!Object.HasInputAuthority) return; if (playerData == null || playerData.PlayerRole != _Role.Trapper) return; // Handle cooldown timers update UpdateCooldowns(); // 1. Input: Select Trap Type via Alpha Keys (1, 2, 3, 4) HandleTrapSelectionInput(); // 2. Input: Toggle Preview (Q key) if (Input.GetKeyDown(KeyCode.Q)) { TogglePreview(); } // 3. Update Preview & Handle Placement Click if (isPreviewing) { UpdateGhostPosition(); if (Input.GetMouseButtonDown(0)) // Left Click to Place { TryPlaceTrap(); } else if (Input.GetMouseButtonDown(1)) // Right Click to Cancel { CancelPreview(); } } } private void UpdateCooldowns() { for (int i = 0; i < cooldownTimers.Length; ++i) { if (cooldownTimers[i] > 0) { cooldownTimers[i] -= Time.deltaTime; } } } private void HandleTrapSelectionInput() { for (int i = 0; i < availableTraps.Length; ++i) { if (Input.GetKeyDown(KeyCode.Alpha1 + i)) { if (selectedTrapIndex != i) { selectedTrapIndex = i; Debug.Log($"Selected trap: {availableTraps[selectedTrapIndex].TrapName}"); // If already previewing, recreate ghost for the new type if (isPreviewing) { DestroyGhost(); CreateGhost(); } } } } } private void TogglePreview() { if (isPreviewing) { CancelPreview(); } else { CreateGhost(); } } private void CreateGhost() { if (selectedTrapIndex < 0 || selectedTrapIndex >= availableTraps.Length) return; var data = availableTraps[selectedTrapIndex]; if (data.GhostPrefab != null) { currentGhostInstance = Instantiate(data.GhostPrefab); isPreviewing = true; } } private void CancelPreview() { DestroyGhost(); isPreviewing = false; } private void DestroyGhost() { if (currentGhostInstance != null) { Destroy(currentGhostInstance); currentGhostInstance = null; } } private void UpdateGhostPosition() { if (currentGhostInstance == null) return; // Perform raycast from viewport center Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); var data = availableTraps[selectedTrapIndex]; if (Physics.Raycast(ray, out RaycastHit hit, data.PlacementMaxDistance, placementLayerMask)) { currentGhostInstance.SetActive(true); currentGhostInstance.transform.position = hit.point; // Align rotation with the surface normal currentGhostInstance.transform.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal); // Validate placement (make sure no walls, players, or other traps block it) bool isValid = ValidatePlacement(hit.point); UpdateGhostVisuals(isValid); } else { currentGhostInstance.SetActive(false); // Hide preview when looking in the air/out of bounds } } private bool ValidatePlacement(Vector3 position) { // Simple check to ensure we don't stack traps on top of each other or clip through walls Collider[] overlaps = Physics.OverlapSphere(position, 0.5f, LayerMask.GetMask("Walls", "Trap")); return overlaps.Length == 0; } private void UpdateGhostVisuals(bool isValid) { if (currentGhostInstance == null) return; // Visual indicator: Green transparency for valid, red for invalid Renderer[] renderers = currentGhostInstance.GetComponentsInChildren(); Color ghostColor = isValid ? new Color(0f, 1f, 0f, 0.4f) : new Color(1f, 0f, 0f, 0.4f); foreach (var r in renderers) { foreach (var mat in r.materials) { // Set standard rendering properties to handle transparent coloring mat.SetFloat("_Mode", 3); // Transparent mode mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); mat.renderQueue = 3000; mat.color = ghostColor; } } } private void TryPlaceTrap() { if (selectedTrapIndex < 0 || selectedTrapIndex >= availableTraps.Length) return; // Check local cooldown if (cooldownTimers[selectedTrapIndex] > 0) { Debug.LogWarning($"{availableTraps[selectedTrapIndex].TrapName} is on cooldown!"); return; } var data = availableTraps[selectedTrapIndex]; Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); if (Physics.Raycast(ray, out RaycastHit hit, data.PlacementMaxDistance, placementLayerMask)) { if (ValidatePlacement(hit.point)) { // Send placement request RPC to host RPC_RequestPlaceTrap(hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal), selectedTrapIndex); // Set cooldown timer cooldownTimers[selectedTrapIndex] = data.Cooldown; CancelPreview(); } } } [Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)] private void RPC_RequestPlaceTrap(Vector3 position, Quaternion rotation, int trapIndex) { if (trapIndex < 0 || trapIndex >= availableTraps.Length) return; TrapDataSO data = availableTraps[trapIndex]; // Host spawns the networked trap prefab NetworkObject spawnedTrapObj = Runner.Spawn(data.TrapPrefab, position, rotation, Object.InputAuthority); // Configure ownership TrapBase trap = spawnedTrapObj.GetComponent(); if (trap != null) { trap.Owner = Object.InputAuthority; } Debug.Log($"[Host] Spawned trap '{data.TrapName}' requested by Player {Object.InputAuthority.PlayerId}"); } private void OnDestroy() { DestroyGhost(); } }