using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; namespace Northbound { public class BuildingPlacement : NetworkBehaviour { [Header("Settings")] public LayerMask groundLayer; public float maxPlacementDistance = 100f; [Header("Preview Materials")] public Material validMaterial; public Material invalidMaterial; [Header("Current Selection")] public int selectedBuildingIndex = 0; [Header("Debug Visualization")] public bool showGridBounds = true; private bool isBuildModeActive = false; private GameObject previewObject; private int currentRotation = 0; // 0-3 private Material[] originalMaterials; private Renderer[] previewRenderers; private PlayerInputActions _inputActions; public override void OnNetworkSpawn() { if (!IsOwner) return; _inputActions = new PlayerInputActions(); _inputActions.Player.ToggleBuildMode.performed += OnToggleBuildMode; _inputActions.Player.Rotate.performed += OnRotate; _inputActions.Player.Build.performed += OnBuild; _inputActions.Enable(); // Create default materials if not assigned if (validMaterial == null) { validMaterial = CreateGhostMaterial(new Color(0, 1, 0, 0.5f)); } if (invalidMaterial == null) { invalidMaterial = CreateGhostMaterial(new Color(1, 0, 0, 0.5f)); } } private Material CreateGhostMaterial(Color color) { // Try to find appropriate shader for current render pipeline Shader shader = Shader.Find("Universal Render Pipeline/Lit"); if (shader == null) shader = Shader.Find("Standard"); if (shader == null) shader = Shader.Find("Diffuse"); Material mat = new Material(shader); // Set base color if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); // URP else if (mat.HasProperty("_Color")) mat.SetColor("_Color", color); // Standard // Enable transparency if (mat.HasProperty("_Surface")) { // URP Transparency mat.SetFloat("_Surface", 1); // Transparent mat.SetFloat("_Blend", 0); // Alpha mat.SetFloat("_AlphaClip", 0); mat.SetFloat("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetFloat("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetFloat("_ZWrite", 0); mat.renderQueue = 3000; mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); mat.EnableKeyword("_ALPHAPREMULTIPLY_ON"); } else { // Standard RP Transparency 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; } return mat; } public override void OnNetworkDespawn() { if (IsOwner && _inputActions != null) { _inputActions.Player.ToggleBuildMode.performed -= OnToggleBuildMode; _inputActions.Player.Rotate.performed -= OnRotate; _inputActions.Player.Build.performed -= OnBuild; _inputActions.Disable(); _inputActions.Dispose(); } } private void Update() { if (!IsOwner) return; if (isBuildModeActive) { UpdatePreviewPosition(); } } private void OnDrawGizmos() { if (!IsOwner || !isBuildModeActive || !showGridBounds) return; if (BuildingManager.Instance == null || previewObject == null || !previewObject.activeSelf) return; BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex]; if (data == null) return; // Draw grid bounds being used for collision detection Bounds bounds = BuildingManager.Instance.GetPlacementBounds(data, previewObject.transform.position, currentRotation); // Check if valid bool isValid = BuildingManager.Instance.IsValidPlacement(data, previewObject.transform.position, currentRotation, out _); Gizmos.color = isValid ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f); Gizmos.DrawWireCube(bounds.center, bounds.size); // Draw grid cells Gizmos.color = Color.yellow; float gridSize = BuildingManager.Instance.gridSize; Vector3 snappedPos = BuildingManager.Instance.SnapToGrid(previewObject.transform.position); // Draw grid origin point Gizmos.DrawSphere(snappedPos, 0.1f); // Draw grid outline for each cell int width = Mathf.RoundToInt(bounds.size.x / gridSize); int length = Mathf.RoundToInt(bounds.size.z / gridSize); for (int x = 0; x < width; x++) { for (int z = 0; z < length; z++) { Vector3 cellPos = snappedPos + new Vector3( (x - width / 2f + 0.5f) * gridSize, 0.01f, (z - length / 2f + 0.5f) * gridSize ); Gizmos.DrawWireCube(cellPos, new Vector3(gridSize, 0.01f, gridSize)); } } } private void OnToggleBuildMode(InputAction.CallbackContext context) { ToggleBuildMode(); } private void OnRotate(InputAction.CallbackContext context) { if (!isBuildModeActive) return; float rotateValue = context.ReadValue(); if (rotateValue > 0) { currentRotation = (currentRotation + 1) % 4; } else if (rotateValue < 0) { currentRotation = (currentRotation - 1 + 4) % 4; } } private void OnBuild(InputAction.CallbackContext context) { if (!isBuildModeActive) return; TryPlaceBuilding(); } private void ToggleBuildMode() { isBuildModeActive = !isBuildModeActive; if (isBuildModeActive) { EnterBuildMode(); } else { ExitBuildMode(); } } private void EnterBuildMode() { currentRotation = 0; CreatePreview(); } private void ExitBuildMode() { DestroyPreview(); } private void CreatePreview() { if (BuildingManager.Instance == null) return; if (BuildingManager.Instance.availableBuildings.Count == 0) return; selectedBuildingIndex = Mathf.Clamp(selectedBuildingIndex, 0, BuildingManager.Instance.availableBuildings.Count - 1); BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex]; if (data == null || data.prefab == null) return; previewObject = Instantiate(data.prefab); previewObject.name = "BuildingPreview"; // Disable colliders on preview foreach (var collider in previewObject.GetComponentsInChildren()) { collider.enabled = false; } // Remove NetworkObject from preview if exists NetworkObject netObj = previewObject.GetComponent(); if (netObj != null) { Destroy(netObj); } // Store original materials and setup preview renderers previewRenderers = previewObject.GetComponentsInChildren(); if (previewRenderers.Length == 0) { // Add a debug cube so you can at least see something GameObject debugCube = GameObject.CreatePrimitive(PrimitiveType.Cube); debugCube.transform.SetParent(previewObject.transform); debugCube.transform.localPosition = Vector3.zero; Destroy(debugCube.GetComponent()); previewRenderers = previewObject.GetComponentsInChildren(); } originalMaterials = new Material[previewRenderers.Length]; for (int i = 0; i < previewRenderers.Length; i++) { originalMaterials[i] = previewRenderers[i].material; // Replace all materials with ghost material Material[] mats = new Material[previewRenderers[i].materials.Length]; for (int j = 0; j < mats.Length; j++) { mats[j] = validMaterial; } previewRenderers[i].materials = mats; // Disable shadow casting previewRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; previewRenderers[i].receiveShadows = false; } } private void DestroyPreview() { if (previewObject != null) { Destroy(previewObject); previewObject = null; } } private void UpdatePreviewPosition() { if (BuildingManager.Instance == null) return; if (previewObject == null) { CreatePreview(); if (previewObject == null) return; } if (Mouse.current == null) return; Vector2 mousePos = Mouse.current.position.ReadValue(); Ray ray = Camera.main.ScreenPointToRay(mousePos); Vector3 targetPosition = Vector3.zero; bool foundPosition = false; // Robust approach: Find where ray intersects ground plane, then raycast down from there // This works even when pointing at empty space between buildings // Step 1: Calculate where ray intersects a horizontal plane at Y=0 // Using plane intersection math Plane groundPlane = new Plane(Vector3.up, Vector3.zero); float enter; if (groundPlane.Raycast(ray, out enter)) { // Get the XZ position where ray hits the ground plane Vector3 planeHitPoint = ray.GetPoint(enter); // Step 2: Cast down from above this XZ position to find actual ground Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z); if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer)) { // Always use the hit point targetPosition = downHit.point; foundPosition = true; } else { // No ground found via raycast, use the plane intersection point directly targetPosition = planeHitPoint; foundPosition = true; } } else { // Fallback: Try direct raycast to anything if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance)) { targetPosition = anyHit.point; foundPosition = true; } } if (foundPosition) { BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex]; // IsValidPlacement now returns the snapped position in groundPosition bool isValid = BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition); previewObject.transform.position = snappedPosition + data.placementOffset; previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0); if (!previewObject.activeSelf) { previewObject.SetActive(true); } // Update material Material previewMat = isValid ? validMaterial : invalidMaterial; foreach (var renderer in previewRenderers) { Material[] mats = new Material[renderer.materials.Length]; for (int i = 0; i < mats.Length; i++) { mats[i] = previewMat; } renderer.materials = mats; } } else { if (previewObject.activeSelf) { previewObject.SetActive(false); } } } private void TryPlaceBuilding() { if (previewObject == null || !previewObject.activeSelf) return; Vector2 mousePos = Mouse.current.position.ReadValue(); Ray ray = Camera.main.ScreenPointToRay(mousePos); Vector3 targetPosition = Vector3.zero; bool foundPosition = false; // Use plane intersection to get XZ position, then raycast down to find ground Plane groundPlane = new Plane(Vector3.up, Vector3.zero); float enter; if (groundPlane.Raycast(ray, out enter)) { Vector3 planeHitPoint = ray.GetPoint(enter); Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z); if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer)) { if (downHit.collider.GetComponentInParent() == null) { targetPosition = downHit.point; foundPosition = true; } else { targetPosition = downHit.point; foundPosition = true; } } else { targetPosition = planeHitPoint; foundPosition = true; } } else { // Fallback: direct raycast if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance)) { targetPosition = anyHit.point; foundPosition = true; } } if (foundPosition) { BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex]; // IsValidPlacement now returns the snapped position if (BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition)) { BuildingManager.Instance.PlaceBuildingServerRpc(selectedBuildingIndex, snappedPosition, currentRotation); } } } override public void OnDestroy() { DestroyPreview(); if (_inputActions != null) { _inputActions.Dispose(); } base.OnDestroy(); } } }