using UnityEngine; using System.Collections.Generic; // МОДУЛЬ ПОСЕЛЕНИЯ. Документ: Docs/Modules/Settlement.md // Управляет режимом строительства. Теперь здание можно поставить ТОЛЬКО // на заранее подготовленный участок (BuildSlot) -- свободное размещение убрано. // Наведение на пустой слот -> подсветка (можно строить). // Наведение на занятый слот / мимо слотов -> подсветки нет, клик ничего не делает. public class BuildManager : MonoBehaviour { public static BuildManager Instance { get; private set; } [Header("Все типы зданий в игре")] public List allBuildings; [Header("Поиск участков под застройку")] [Tooltip("Слой, на котором находятся объекты BuildSlot")] public LayerMask buildSlotsLayerMask; [Header("Подсветка участка при наведении")] public Color highlightValidColor = new Color(1f, 1f, 1f, 0.9f); // белая -- можно строить public Color highlightInvalidColor = new Color(1f, 0.2f, 0.2f, 0.9f); // красная -- нельзя (не хватает ресурсов) private BuildingData currentlyPlacing; private BuildSlot lastHighlightedSlot; private Camera mainCam; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; mainCam = Camera.main; } void Start() { // Повторный вход в город (например, с карты мира): здания восстанавливаем из памяти модуля SettlementModule.ApplyToScene(); } void Update() { if (currentlyPlacing == null) return; // не в режиме строительства -- ничего не делаем Vector3 worldPos = GetPointerWorldPosition(); worldPos.z = 0; BuildSlot slotUnderPointer = GetSlotAtPosition(worldPos); // Убираем подсветку с предыдущего слота, если курсор ушёл с него if (lastHighlightedSlot != null && lastHighlightedSlot != slotUnderPointer) { lastHighlightedSlot.ClearHighlight(); } if (slotUnderPointer != null && !slotUnderPointer.isOccupied) { bool canAfford = currentlyPlacing.CanAfford(); slotUnderPointer.SetHighlight(canAfford ? highlightValidColor : highlightInvalidColor); lastHighlightedSlot = slotUnderPointer; if (Input.GetMouseButtonDown(0) && !UIBlocker.IsPointerOverUI()) { TryConfirmPlacement(slotUnderPointer, canAfford); } } else { lastHighlightedSlot = null; } // Отмена по правой кнопке / Escape if (Input.GetMouseButtonDown(1) || Input.GetKeyDown(KeyCode.Escape)) { CancelPlacement(); } } BuildSlot GetSlotAtPosition(Vector3 position) { Collider2D hit = Physics2D.OverlapPoint(position, buildSlotsLayerMask); if (hit == null) return null; return hit.GetComponent(); } // Список зданий, доступных для стройки ПРЯМО СЕЙЧАС (по уровню Ратуши) public List GetAvailableBuildings() { List available = new List(); int townHallLevel = TownHall.Instance != null ? TownHall.Instance.currentLevel : 1; foreach (var b in allBuildings) { if (b.requiredTownHallLevel <= townHallLevel) available.Add(b); } return available; } // Вызывается из UI-кнопки конкретного здания в меню стройки public void StartPlacing(BuildingData data) { currentlyPlacing = data; Debug.Log($"[СТРОЙКА] Выбрано здание: {data.buildingName}. Кликни на пустой участок."); } void TryConfirmPlacement(BuildSlot slot, bool canAfford) { if (!canAfford) { Debug.Log("[СТРОЙКА] Не хватает ресурсов!"); return; } currentlyPlacing.PayCost(); GameObject built = Instantiate(currentlyPlacing.prefab, slot.transform.position, Quaternion.identity); if (!built.activeSelf) built.SetActive(true); // шаблон казарм хранится выключенным PlacedBuildingId idMarker = built.AddComponent(); idMarker.buildingName = currentlyPlacing.buildingName; slot.Occupy(built); BuildingSlotLink link = built.AddComponent(); link.linkedSlot = slot; Debug.Log($"[СТРОЙКА] Построено: {currentlyPlacing.buildingName} на участке {slot.name}"); EventBus.Publish(new BuildingPlacedEvent { BuildingName = currentlyPlacing.buildingName }); currentlyPlacing = null; lastHighlightedSlot = null; } void CancelPlacement() { if (lastHighlightedSlot != null) { lastHighlightedSlot.ClearHighlight(); lastHighlightedSlot = null; } currentlyPlacing = null; Debug.Log("[СТРОЙКА] Размещение отменено."); } // ===== Новая схема стройки: тап по участку -> выбор здания (BuildModeController) ===== // Построить здание сразу на указанном участке. true -- получилось. public bool BuildOnSlot(BuildingData data, BuildSlot slot) { if (data == null || slot == null || slot.isOccupied) return false; if (!data.CanAfford()) return false; currentlyPlacing = data; TryConfirmPlacement(slot, true); return true; } // Участок под точкой экрана (с допуском, чтобы попадать с первого раза пальцем) public BuildSlot SlotAtScreen(Vector2 screenPos, float tolerance = 0.35f) { if (mainCam == null) mainCam = Camera.main; if (mainCam == null) return null; Vector3 w = mainCam.ScreenToWorldPoint(screenPos); Vector2 p = new Vector2(w.x, w.y); // Перебираем все участки и берём ближайший: работает и с триггер-коллайдерами, и без настроенного слоя BuildSlot best = null; float bestDist = tolerance; foreach (var slot in FindObjectsByType(FindObjectsSortMode.None)) { var col = slot.GetComponent(); Bounds b = col != null ? col.bounds : new Bounds(slot.transform.position, Vector3.one * 0.5f); b.center = new Vector3(b.center.x, b.center.y, 0f); float d = Mathf.Sqrt(b.SqrDistance(new Vector3(p.x, p.y, 0f))); if (d <= bestDist) { bestDist = d; best = slot; } } return best; } // Найти пустой (свободный) BuildSlot по названию -- используется при восстановлении сохранения public BuildSlot FindSlotAtPosition(Vector3 position, float tolerance = 0.05f) { BuildSlot[] allSlots = FindObjectsByType(FindObjectsSortMode.None); foreach (var slot in allSlots) { if (Vector3.Distance(slot.transform.position, position) < tolerance) return slot; } return null; } Vector3 GetPointerWorldPosition() { Vector3 screenPos = Input.mousePosition; return mainCam.ScreenToWorldPoint(screenPos); } }