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; } /// /// 건물 배치 요청 (클라이언트에서 호출) /// public void RequestPlaceBuilding(int buildingIndex, Vector3 position, int rotation) { if (!NetworkManager.Singleton.IsClient) { Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다."); return; } ulong clientId = NetworkManager.Singleton.LocalClientId; PlaceBuildingServerRpc(buildingIndex, position, rotation, clientId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId) { // 보안 검증 1: 유효한 클라이언트인지 확인 if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId)) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}"); return; } // 보안 검증 2: 건물 인덱스 유효성 확인 if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex} (클라이언트: {requestingClientId})"); return; } BuildingData data = availableBuildings[buildingIndex]; // 보안 검증 3: 건물 데이터 유효성 확인 if (data == null || data.prefab == null) { Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다. (클라이언트: {requestingClientId})"); return; } // 배치 가능 여부 확인 if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition)) { Debug.LogWarning($"[BuildingManager] 건물 배치 불가능 위치 (클라이언트: {requestingClientId})"); return; } Vector3Int gridPosition = WorldToGrid(snappedPosition); // 건물 생성 GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); NetworkObject netObj = buildingObj.GetComponent(); if (netObj != null) { // 건물의 소유자를 설정 netObj.SpawnWithOwnership(requestingClientId); Building building = buildingObj.GetComponent(); if (building == null) { building = buildingObj.AddComponent(); } // 건물 초기화 building.Initialize(data, gridPosition, rotation, requestingClientId); placedBuildings.Add(building); Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})"); } else { Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})"); Destroy(buildingObj); } } public void RemoveBuilding(Building building) { if (placedBuildings.Contains(building)) { placedBuildings.Remove(building); Debug.Log($"[BuildingManager] 건물 제거됨: {building.buildingData?.buildingName ?? "Unknown"}"); } } /// /// 사전 배치 건물 등록 (씬에 미리 있는 건물용) /// public void RegisterPrePlacedBuilding(Building building) { if (!placedBuildings.Contains(building)) { placedBuildings.Add(building); Debug.Log($"[BuildingManager] 사전 배치 건물 등록: {building.buildingData?.buildingName ?? building.gameObject.name}"); } } /// /// 특정 위치에 건물이 있는지 확인 /// public Building GetBuildingAtPosition(Vector3Int gridPosition) { foreach (var building in placedBuildings) { if (building != null && building.gridPosition == gridPosition) { return building; } } return null; } /// /// 특정 소유자의 모든 건물 가져오기 /// public List GetBuildingsByOwner(ulong ownerId) { List ownedBuildings = new List(); foreach (var building in placedBuildings) { if (building != null && building.GetOwnerId() == ownerId) { ownedBuildings.Add(building); } } return ownedBuildings; } } }