using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; namespace MedievalShopkeeper { public class BuildManager : MonoBehaviour { public static BuildManager Instance { get; private set; } [Header("Grid Settings")] [SerializeField] private float gridSize = 1f; [Header("Layer Masks")] [Tooltip("The layer(s) representing player-owned floor.")] [SerializeField] private LayerMask floorLayer; [Tooltip("The layer(s) representing player-owned walls.")] [SerializeField] private LayerMask wallLayer; [Tooltip("Layers that block placement (e.g., other shelves, NPCs, Default objects).")] [SerializeField] private LayerMask obstacleLayers; // State public bool IsBuilding { get; private set; } private PlaceableData currentData; private GameObject ghostObject; private List ghostRenderers = new List(); private Material validMaterial; private Material invalidMaterial; private bool isValidPlacement; private Camera mainCam; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; CreateGhostMaterials(); mainCam = Camera.main; } private void CreateGhostMaterials() { // Create a procedural transparent green material validMaterial = new Material(Shader.Find("Standard")); validMaterial.SetFloat("_Mode", 2); // Fade mode validMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); validMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); validMaterial.SetInt("_ZWrite", 0); validMaterial.DisableKeyword("_ALPHATEST_ON"); validMaterial.EnableKeyword("_ALPHABLEND_ON"); validMaterial.DisableKeyword("_ALPHAPREMULTIPLY_ON"); validMaterial.renderQueue = 3000; validMaterial.color = new Color(0f, 1f, 0f, 0.4f); // Copy and make red invalidMaterial = new Material(validMaterial); invalidMaterial.color = new Color(1f, 0f, 0f, 0.4f); } public void StartBuilding(PlaceableData data) { if (data == null || data.prefab == null) return; CancelBuilding(); // clean up any existing currentData = data; IsBuilding = true; // Close any UI menus so they don't block the screen if (UIManager.Instance != null) UIManager.Instance.CloseAllMenus(); // Create Ghost ghostObject = Instantiate(data.prefab); ghostObject.name = "Ghost_" + data.objectName; // Strip colliders and scripts from ghost so it doesn't interact physically foreach (var col in ghostObject.GetComponentsInChildren()) Destroy(col); foreach (var comp in ghostObject.GetComponentsInChildren()) Destroy(comp); // If it has a NavMeshObstacle, we must destroy it on the ghost so it doesn't carve var navObstacle = ghostObject.GetComponent(); if (navObstacle != null) Destroy(navObstacle); // Collect renderers for material swapping ghostRenderers.Clear(); ghostRenderers.AddRange(ghostObject.GetComponentsInChildren()); UpdateGhostMaterial(true); // Default green } public void CancelBuilding() { IsBuilding = false; currentData = null; if (ghostObject != null) { Destroy(ghostObject); } ghostRenderers.Clear(); } private void Update() { if (!IsBuilding || ghostObject == null) return; bool rightClick = Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame; bool escapeKey = Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame; // Cancel on Right Click or Escape if (rightClick || escapeKey) { CancelBuilding(); return; } Vector2 pointerPos = Mouse.current != null ? Mouse.current.position.ReadValue() : Vector2.zero; // Raycast from mouse to find placement surface Ray ray = mainCam.ScreenPointToRay(pointerPos); LayerMask targetMask = (currentData.placementType == PlacementType.Floor) ? floorLayer : wallLayer; if (Physics.Raycast(ray, out RaycastHit hit, 100f, targetMask)) { ghostObject.SetActive(true); Vector3 targetPos; Quaternion targetRot = ghostObject.transform.rotation; if (currentData.placementType == PlacementType.Floor) { // Grid Snapping for Floor float x = Mathf.Round(hit.point.x / gridSize) * gridSize; float z = Mathf.Round(hit.point.z / gridSize) * gridSize; targetPos = new Vector3(x, hit.point.y, z); // Allow user to rotate with R key bool rKey = Keyboard.current != null && Keyboard.current.rKey.wasPressedThisFrame; if (rKey) { ghostObject.transform.Rotate(0, 90f, 0); } targetRot = ghostObject.transform.rotation; } else { // Wall placement - align with wall normal targetRot = Quaternion.LookRotation(hit.normal); // Faces exactly outward from the wall // Restrict height to 1.0 or 1.5 (whichever is closer to the mouse) float snappedY = Mathf.Abs(hit.point.y - 1.0f) < Mathf.Abs(hit.point.y - 1.5f) ? 1.0f : 1.5f; targetPos = new Vector3(hit.point.x, snappedY, hit.point.z); } ghostObject.transform.position = targetPos; ghostObject.transform.rotation = targetRot; // Validate Placement CheckPlacementValidity(targetPos, targetRot); // Place on Left Click bool leftClick = Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame; if (leftClick) { // Prevent clicking through UI buttons if (!EventSystem.current.IsPointerOverGameObject()) { TryPlaceObject(); } } } else { // Hide ghost if mouse is not over a valid surface layer ghostObject.SetActive(false); } } private void CheckPlacementValidity(Vector3 pos, Quaternion rot) { // Center of the box might need an offset if the prefab pivot is at the bottom. // For now, we assume pivot is at the bottom, so we shift the box center up by half its extents. Vector3 boxCenter = pos + rot * new Vector3(0, currentData.overlapBoxExtents.y, 0); // Check if blocked by obstacles Collider[] colliders = Physics.OverlapBox(boxCenter, currentData.overlapBoxExtents, rot, obstacleLayers); isValidPlacement = (colliders.Length == 0); // Optional: Also check if player can afford it if (GameManager.Instance != null && GameManager.Instance.CurrentMoney < currentData.purchaseCost) { isValidPlacement = false; } UpdateGhostMaterial(isValidPlacement); } private void UpdateGhostMaterial(bool isValid) { Material targetMat = isValid ? validMaterial : invalidMaterial; foreach (var r in ghostRenderers) { if (r == null) continue; // Replace all materials on this renderer with the target colored material Material[] mats = new Material[r.materials.Length]; for (int i = 0; i < mats.Length; i++) { mats[i] = targetMat; } r.materials = mats; } } private void TryPlaceObject() { if (!isValidPlacement) { Debug.LogWarning("[BuildManager] Cannot place object here (Blocked or insufficient funds)."); return; } if (GameManager.Instance != null && !GameManager.Instance.TrySpendMoney(currentData.purchaseCost)) { return; // Failed to afford } // Spawn the real object GameObject newObj = Instantiate(currentData.prefab, ghostObject.transform.position, ghostObject.transform.rotation); newObj.name = currentData.objectName; Debug.Log($"[BuildManager] Placed {currentData.objectName} for {currentData.purchaseCost} G."); // Keep building mode active so they can place multiple, just validate again immediately CheckPlacementValidity(ghostObject.transform.position, ghostObject.transform.rotation); } } }