Files
MedievalShopkeeper/Assets/Scripts/BuildManager.cs
2026-09-03 16:35:05 +01:00

247 lines
9.4 KiB
C#

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<Renderer> ghostRenderers = new List<Renderer>();
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<Collider>()) Destroy(col);
foreach (var comp in ghostObject.GetComponentsInChildren<MonoBehaviour>()) Destroy(comp);
// If it has a NavMeshObstacle, we must destroy it on the ghost so it doesn't carve
var navObstacle = ghostObject.GetComponent<UnityEngine.AI.NavMeshObstacle>();
if (navObstacle != null) Destroy(navObstacle);
// Collect renderers for material swapping
ghostRenderers.Clear();
ghostRenderers.AddRange(ghostObject.GetComponentsInChildren<Renderer>());
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);
}
}
}