247 lines
9.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
|