2026-09-03 16:35:05 +01:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
|
|
|
|
namespace MedievalShopkeeper
|
|
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
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; }
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
[Header("Settings")]
|
|
|
|
|
[Tooltip("Size of the grid for snapping floor placements.")]
|
2026-09-03 16:35:05 +01:00
|
|
|
[SerializeField] private float gridSize = 1f;
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
[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;
|
2026-09-04 16:33:41 +01:00
|
|
|
|
|
|
|
|
[Tooltip("Layer representing the walls.")]
|
2026-09-03 16:35:05 +01:00
|
|
|
[SerializeField] private LayerMask wallLayer;
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
[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;
|
2026-09-04 16:33:41 +01:00
|
|
|
private GameObject objectBeingMoved;
|
2026-09-03 16:35:05 +01:00
|
|
|
private List<Renderer> ghostRenderers = new List<Renderer>();
|
|
|
|
|
private bool isValidPlacement;
|
|
|
|
|
private Camera mainCam;
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
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;
|
2026-09-04 16:33:41 +01:00
|
|
|
|
|
|
|
|
if (validMaterial == null) CreateDefaultMaterials();
|
2026-09-03 16:35:05 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
private void CreateDefaultMaterials()
|
2026-09-03 16:35:05 +01:00
|
|
|
{
|
|
|
|
|
validMaterial = new Material(Shader.Find("Standard"));
|
2026-09-04 16:33:41 +01:00
|
|
|
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;
|
2026-09-04 16:33:41 +01:00
|
|
|
CancelBuilding();
|
2026-09-03 16:35:05 +01:00
|
|
|
currentData = data;
|
2026-09-04 16:33:41 +01:00
|
|
|
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;
|
2026-09-04 16:33:41 +01:00
|
|
|
PrepareGhostObject(ghostObject);
|
|
|
|
|
}
|
2026-09-03 16:35:05 +01:00
|
|
|
|
2026-09-04 16:33:41 +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
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
var navObstacle = obj.GetComponent<UnityEngine.AI.NavMeshObstacle>();
|
2026-09-03 16:35:05 +01:00
|
|
|
if (navObstacle != null) Destroy(navObstacle);
|
|
|
|
|
|
|
|
|
|
ghostRenderers.Clear();
|
2026-09-04 16:33:41 +01:00
|
|
|
ghostRenderers.AddRange(obj.GetComponentsInChildren<Renderer>());
|
|
|
|
|
UpdateGhostMaterial(true);
|
2026-09-03 16:35:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CancelBuilding()
|
|
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
currentMode = BuildMode.None;
|
2026-09-03 16:35:05 +01:00
|
|
|
currentData = null;
|
|
|
|
|
if (ghostObject != null)
|
|
|
|
|
{
|
|
|
|
|
Destroy(ghostObject);
|
|
|
|
|
}
|
2026-09-04 16:33:41 +01:00
|
|
|
if (objectBeingMoved != null)
|
|
|
|
|
{
|
|
|
|
|
objectBeingMoved.SetActive(true);
|
|
|
|
|
objectBeingMoved = null;
|
|
|
|
|
}
|
2026-09-03 16:35:05 +01:00
|
|
|
ghostRenderers.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
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;
|
2026-09-04 16:33:41 +01:00
|
|
|
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);
|
2026-09-04 16:33:41 +01:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
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
|
|
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
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);
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
if (leftClick && !EventSystem.current.IsPointerOverGameObject())
|
2026-09-03 16:35:05 +01:00
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
TryPlaceObject();
|
2026-09-03 16:35:05 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ghostObject.SetActive(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
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)
|
|
|
|
|
{
|
2026-09-04 16:33:41 +01:00
|
|
|
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
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
Collider[] colliders = Physics.OverlapBox(boxCenter, extents, rot, obstacleLayers);
|
2026-09-03 16:35:05 +01:00
|
|
|
isValidPlacement = (colliders.Length == 0);
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
if (objectBeingMoved == null && currentData != null)
|
2026-09-03 16:35:05 +01:00
|
|
|
{
|
2026-09-04 16:33:41 +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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
if (objectBeingMoved != null)
|
2026-09-03 16:35:05 +01:00
|
|
|
{
|
2026-09-04 16:33:41 +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
|
|
|
}
|
2026-09-04 16:33:41 +01:00
|
|
|
else if (currentData != null)
|
|
|
|
|
{
|
|
|
|
|
if (GameManager.Instance != null && !GameManager.Instance.TrySpendMoney(currentData.purchaseCost))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-09-03 16:35:05 +01:00
|
|
|
|
2026-09-04 16:33:41 +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
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
Debug.Log($"[BuildManager] Placed {currentData.objectName} for {currentData.purchaseCost} G.");
|
2026-09-03 16:35:05 +01:00
|
|
|
|
2026-09-04 16:33:41 +01:00
|
|
|
CheckPlacementValidity(ghostObject.transform.position, ghostObject.transform.rotation);
|
|
|
|
|
}
|
2026-09-03 16:35:05 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|