using System.Collections.Generic; using Unity.Netcode; using UnityEngine; namespace Northbound { public class BuildingManager : NetworkBehaviour { public static BuildingManager Instance { get; private set; } [Header("Settings")] public float gridSize = 1f; public LayerMask groundLayer; [Header("Building Database")] public List availableBuildings = new List(); private List placedBuildings = new List(); private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } public Vector3 SnapToGrid(Vector3 worldPosition) { return new Vector3( Mathf.Round(worldPosition.x / gridSize) * gridSize, worldPosition.y, Mathf.Round(worldPosition.z / gridSize) * gridSize ); } public Vector3Int WorldToGrid(Vector3 worldPosition) { return new Vector3Int( Mathf.RoundToInt(worldPosition.x / gridSize), Mathf.RoundToInt(worldPosition.y / gridSize), Mathf.RoundToInt(worldPosition.z / gridSize) ); } public Vector3 GridToWorld(Vector3Int gridPosition) { return new Vector3( gridPosition.x * gridSize, gridPosition.y * gridSize, gridPosition.z * gridSize ); } public bool IsValidPlacement(BuildingData data, Vector3 position, int rotation, out Vector3 groundPosition) { groundPosition = position; // Ground check if (!CheckGround(position, out groundPosition)) return false; // IMPORTANT: Snap to grid BEFORE checking overlap! // Otherwise we check the raw cursor position which might overlap Vector3 snappedPosition = SnapToGrid(groundPosition); groundPosition = snappedPosition; // Update groundPosition to snapped value // Overlap check using GRID SIZE from BuildingData (not actual colliders) Vector3 gridSize = data.GetSize(rotation); // Shrink bounds slightly to allow buildings to touch without overlapping // This prevents Bounds.Intersects() from returning true for adjacent buildings Vector3 shrunkSize = gridSize - Vector3.one * 0.01f; Bounds checkBounds = new Bounds(snappedPosition + Vector3.up * gridSize.y * 0.5f, shrunkSize); foreach (var building in placedBuildings) { if (building == null) continue; // Compare grid bounds, not collider bounds Bounds buildingGridBounds = building.GetGridBounds(); if (checkBounds.Intersects(buildingGridBounds)) return false; } return true; } /// /// Get the grid bounds for a building at a given position /// Useful for preview visualization /// public Bounds GetPlacementBounds(BuildingData data, Vector3 position, int rotation) { Vector3 gridSize = data.GetSize(rotation); return new Bounds(position + Vector3.up * gridSize.y * 0.5f, gridSize); } private bool CheckGround(Vector3 position, out Vector3 groundPosition) { groundPosition = position; // Raycast down to find ground if (Physics.Raycast(position + Vector3.up * 10f, Vector3.down, out RaycastHit hit, 20f, groundLayer)) { groundPosition = hit.point; return true; } return false; } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] public void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation) { if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count) return; BuildingData data = availableBuildings[buildingIndex]; // IsValidPlacement now returns snapped position in groundPosition if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition)) return; Vector3Int gridPosition = WorldToGrid(snappedPosition); // Spawn building at snapped position GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); NetworkObject netObj = buildingObj.GetComponent(); if (netObj != null) { netObj.Spawn(); Building building = buildingObj.GetComponent(); if (building == null) building = buildingObj.AddComponent(); building.Initialize(data, gridPosition, rotation); placedBuildings.Add(building); } } public void RemoveBuilding(Building building) { if (placedBuildings.Contains(building)) placedBuildings.Remove(building); } } }