// Assets/_Project/Scripts/Gameplay/BuilderInputController.cs using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using TD.Core; namespace TD.Gameplay { /// /// Owner-only client-side controller for builder input. Handles right-click-to-move, /// deferring to placement mode (right-click cancels placement instead). /// /// /// Owner-only. This component lives on the same GameObject as /// but only its owning client processes input. Non-owner clients /// have this component but its Update is a no-op. The owner sends move requests via /// . /// /// Right-click priority. If TowerPlacementController.IsPlacing is /// true, right-click cancels placement (handled by TowerPlacementController /// itself). When NOT placing, right-click moves the builder. /// /// Raycast target. The cursor is raycast against the BuildablePlane layer /// (same as placement). The hit point's XZ is sent as the target; Y is recomputed by /// the server via terrain raycast. /// public class BuilderInputController : NetworkBehaviour { // ----- Inspector -------------------------------------------------- [Tooltip("Physics layer mask for the BuildablePlane collider. Cursor is raycast " + "against this layer to determine the move target.")] [SerializeField] private LayerMask buildablePlaneLayerMask; [Tooltip("Maximum raycast distance for cursor → world conversion.")] [SerializeField] private float raycastMaxDistance = 500f; // Cached reference to the sibling Builder component. private Builder builder; // Cached reference to the local TowerPlacementController, looked up lazily because // it may not exist at OnNetworkSpawn time. private TowerPlacementController cachedPlacementController; // ----- Lifecycle -------------------------------------------------- public override void OnNetworkSpawn() { builder = GetComponent(); if (builder == null) { Debug.LogError("[BuilderInputController] Missing Builder component on the " + "same GameObject. Disabling input."); enabled = false; return; } // Non-owners do nothing. if (!IsOwner) { enabled = false; } } // ----- Input handling --------------------------------------------- private void Update() { // Defensive: enabled is set false for non-owners in OnNetworkSpawn, but keep // the IsOwner check here in case order-of-operations changes. if (!IsOwner) return; var mouse = Mouse.current; if (mouse == null) return; if (!mouse.rightButton.wasPressedThisFrame) return; // Defer to placement mode: if the player is placing a tower, right-click cancels // placement rather than moving the builder. TowerPlacementController handles // the cancel itself; we just don't process the click here. if (IsLocalPlayerPlacing()) return; // Cursor → world. if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 worldPoint)) return; // Submit to server. builder.RequestMoveRpc(worldPoint); } // ----- Helpers ---------------------------------------------------- private bool IsLocalPlayerPlacing() { if (cachedPlacementController == null) { // Find lazily — controller may have been added after this component spawned. cachedPlacementController = UnityEngine.Object.FindAnyObjectByType(); if (cachedPlacementController == null) return false; } return cachedPlacementController.IsPlacing; } private bool TryGetBuildablePlaneHit(Vector2 screenPos, out Vector3 hitPoint) { hitPoint = Vector3.zero; var cam = Camera.main; if (cam == null) return false; Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f)); if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, buildablePlaneLayerMask)) { hitPoint = hit.point; return true; } return false; } } }