Files
MedievalShopkeeper/Assets/Scripts/BuildManager.cs

350 lines
13 KiB
C#
Raw Normal View History

2026-09-03 16:35:05 +01:00
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace MedievalShopkeeper
{
public enum BuildMode
{
None,
Build,
Move,
Sell,
Repair
}
2026-09-03 16:35:05 +01:00
public class BuildManager : MonoBehaviour
{
public static BuildManager Instance { get; private set; }
[Header("Settings")]
[Tooltip("Size of the grid for snapping floor placements.")]
2026-09-03 16:35:05 +01:00
[SerializeField] private float gridSize = 1f;
[Tooltip("Layers representing obstacles that block placement.")]
[SerializeField] private LayerMask obstacleLayers;
[Tooltip("Layer representing the floor.")]
2026-09-03 16:35:05 +01:00
[SerializeField] private LayerMask floorLayer;
[Tooltip("Layer representing the walls.")]
2026-09-03 16:35:05 +01:00
[SerializeField] private LayerMask wallLayer;
[Header("Materials")]
[Tooltip("Material shown when placement is valid.")]
[SerializeField] private Material validMaterial;
[Tooltip("Material shown when placement is invalid.")]
[SerializeField] private Material invalidMaterial;
// Current state
private BuildMode currentMode = BuildMode.None;
2026-09-03 16:35:05 +01:00
private PlaceableData currentData;
private GameObject ghostObject;
private GameObject objectBeingMoved;
2026-09-03 16:35:05 +01:00
private List<Renderer> ghostRenderers = new List<Renderer>();
private bool isValidPlacement;
private Camera mainCam;
public bool IsBuilding => currentMode != BuildMode.None;
2026-09-03 16:35:05 +01:00
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
mainCam = Camera.main;
if (validMaterial == null) CreateDefaultMaterials();
2026-09-03 16:35:05 +01:00
}
private void CreateDefaultMaterials()
2026-09-03 16:35:05 +01:00
{
validMaterial = new Material(Shader.Find("Standard"));
validMaterial.SetFloat("_Mode", 2); // Fade
2026-09-03 16:35:05 +01:00
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);
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();
2026-09-03 16:35:05 +01:00
currentData = data;
currentMode = BuildMode.Build;
2026-09-03 16:35:05 +01:00
if (UIManager.Instance != null) UIManager.Instance.CloseAllMenus();
ghostObject = Instantiate(data.prefab);
ghostObject.name = "Ghost_" + data.objectName;
PrepareGhostObject(ghostObject);
}
2026-09-03 16:35:05 +01:00
public void StartMoveMode()
{
CancelBuilding();
currentMode = BuildMode.Move;
Debug.Log("[BuildManager] Move Mode Active.");
}
public void StartSellMode()
{
CancelBuilding();
currentMode = BuildMode.Sell;
Debug.Log("[BuildManager] Sell Mode Active.");
}
public void StartRepairMode()
{
CancelBuilding();
currentMode = BuildMode.Repair;
Debug.Log("[BuildManager] Repair Mode Active.");
}
private void PrepareGhostObject(GameObject obj)
{
foreach (var col in obj.GetComponentsInChildren<Collider>()) Destroy(col);
foreach (var comp in obj.GetComponentsInChildren<MonoBehaviour>()) Destroy(comp);
2026-09-03 16:35:05 +01:00
var navObstacle = obj.GetComponent<UnityEngine.AI.NavMeshObstacle>();
2026-09-03 16:35:05 +01:00
if (navObstacle != null) Destroy(navObstacle);
ghostRenderers.Clear();
ghostRenderers.AddRange(obj.GetComponentsInChildren<Renderer>());
UpdateGhostMaterial(true);
2026-09-03 16:35:05 +01:00
}
public void CancelBuilding()
{
currentMode = BuildMode.None;
2026-09-03 16:35:05 +01:00
currentData = null;
if (ghostObject != null)
{
Destroy(ghostObject);
}
if (objectBeingMoved != null)
{
objectBeingMoved.SetActive(true);
objectBeingMoved = null;
}
2026-09-03 16:35:05 +01:00
ghostRenderers.Clear();
}
private void Update()
{
if (currentMode == BuildMode.None) return;
2026-09-03 16:35:05 +01:00
bool rightClick = Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame;
bool escapeKey = Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame;
bool leftClick = Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
2026-09-03 16:35:05 +01:00
if (rightClick || escapeKey)
{
CancelBuilding();
return;
}
Vector2 pointerPos = Mouse.current != null ? Mouse.current.position.ReadValue() : Vector2.zero;
Ray ray = mainCam.ScreenPointToRay(pointerPos);
if (currentMode == BuildMode.Move || currentMode == BuildMode.Sell || currentMode == BuildMode.Repair)
{
if (leftClick && !EventSystem.current.IsPointerOverGameObject())
{
if (Physics.Raycast(ray, out RaycastHit selectHit, 100f, obstacleLayers))
{
GameObject clickedObj = selectHit.collider.transform.root.gameObject;
if (clickedObj.GetComponentInChildren<Interactable>() != null)
{
HandleExistingObjectClick(clickedObj);
}
}
}
return;
}
if (ghostObject == null || currentData == null && objectBeingMoved == null) return;
LayerMask targetMask = floorLayer;
if (currentData != null && currentData.placementType == PlacementType.Wall) targetMask = wallLayer;
else if (objectBeingMoved != null && objectBeingMoved.CompareTag("WallMount")) targetMask = wallLayer; // Generic fallback
2026-09-03 16:35:05 +01:00
if (Physics.Raycast(ray, out RaycastHit hit, 100f, targetMask))
{
ghostObject.SetActive(true);
Vector3 targetPos;
Quaternion targetRot = ghostObject.transform.rotation;
if (targetMask == floorLayer)
2026-09-03 16:35:05 +01:00
{
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);
bool rKey = Keyboard.current != null && Keyboard.current.rKey.wasPressedThisFrame;
if (rKey)
{
ghostObject.transform.Rotate(0, 90f, 0);
}
targetRot = ghostObject.transform.rotation;
}
else
{
targetRot = Quaternion.LookRotation(hit.normal);
2026-09-03 16:35:05 +01:00
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;
CheckPlacementValidity(targetPos, targetRot);
if (leftClick && !EventSystem.current.IsPointerOverGameObject())
2026-09-03 16:35:05 +01:00
{
TryPlaceObject();
2026-09-03 16:35:05 +01:00
}
}
else
{
ghostObject.SetActive(false);
}
}
private void HandleExistingObjectClick(GameObject obj)
{
if (currentMode == BuildMode.Sell)
{
if (GameManager.Instance != null) GameManager.Instance.AddMoney(50);
Debug.Log($"[BuildManager] Sold {obj.name} for 50G.");
if (AudioManager.Instance != null) AudioManager.Instance.PlayDestroy();
Destroy(obj);
currentMode = BuildMode.Sell;
}
else if (currentMode == BuildMode.Repair)
{
Shelf shelf = obj.GetComponent<Shelf>();
if (shelf != null && shelf.isBroken)
{
shelf.RepairShelf();
if (GameManager.Instance != null && !GameManager.Instance.TrySpendMoney(10))
{
Debug.LogWarning("[BuildManager] Not enough money to repair (costs 10G)!");
shelf.BreakShelf();
}
else
{
if (AudioManager.Instance != null) AudioManager.Instance.PlayRepair();
Debug.Log($"[BuildManager] Repaired {obj.name} for 10G.");
}
}
}
else if (currentMode == BuildMode.Move)
{
objectBeingMoved = obj;
objectBeingMoved.SetActive(false); // Hide real one
if (ghostObject != null) Destroy(ghostObject);
ghostObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
Destroy(ghostObject.GetComponent<Collider>());
ghostObject.transform.localScale = new Vector3(1f, 1f, 1f);
PrepareGhostObject(ghostObject);
currentMode = BuildMode.Build;
Debug.Log($"[BuildManager] Picked up {obj.name} to move.");
}
}
2026-09-03 16:35:05 +01:00
private void CheckPlacementValidity(Vector3 pos, Quaternion rot)
{
Vector3 extents = currentData != null ? currentData.overlapBoxExtents : new Vector3(0.45f, 0.45f, 0.45f);
Vector3 boxCenter = pos + rot * new Vector3(0, extents.y, 0);
2026-09-03 16:35:05 +01:00
Collider[] colliders = Physics.OverlapBox(boxCenter, extents, rot, obstacleLayers);
2026-09-03 16:35:05 +01:00
isValidPlacement = (colliders.Length == 0);
if (objectBeingMoved == null && currentData != null)
2026-09-03 16:35:05 +01:00
{
if (GameManager.Instance != null && GameManager.Instance.CurrentMoney < currentData.purchaseCost)
{
isValidPlacement = false;
}
2026-09-03 16:35:05 +01:00
}
UpdateGhostMaterial(isValidPlacement);
}
private void UpdateGhostMaterial(bool isValid)
{
Material targetMat = isValid ? validMaterial : invalidMaterial;
foreach (var r in ghostRenderers)
{
if (r == null) continue;
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 (objectBeingMoved != null)
2026-09-03 16:35:05 +01:00
{
objectBeingMoved.transform.position = ghostObject.transform.position;
objectBeingMoved.transform.rotation = ghostObject.transform.rotation;
objectBeingMoved.SetActive(true);
if (AudioManager.Instance != null) AudioManager.Instance.PlayBuild();
Debug.Log($"[BuildManager] Moved {objectBeingMoved.name}.");
CancelBuilding();
2026-09-03 16:35:05 +01:00
}
else if (currentData != null)
{
if (GameManager.Instance != null && !GameManager.Instance.TrySpendMoney(currentData.purchaseCost))
{
return;
}
2026-09-03 16:35:05 +01:00
GameObject newObj = Instantiate(currentData.prefab, ghostObject.transform.position, ghostObject.transform.rotation);
newObj.name = currentData.objectName;
if (AudioManager.Instance != null) AudioManager.Instance.PlayBuild();
2026-09-03 16:35:05 +01:00
Debug.Log($"[BuildManager] Placed {currentData.objectName} for {currentData.purchaseCost} G.");
2026-09-03 16:35:05 +01:00
CheckPlacementValidity(ghostObject.transform.position, ghostObject.transform.rotation);
}
2026-09-03 16:35:05 +01:00
}
}
}