using UnityEngine; using System.Collections.Generic; // МОДУЛЬ ПОСЕЛЕНИЯ. Документ: Docs/Modules/Settlement.md // Список зданий и сама постройка. // Сейчас стройка свободная, на сетке: игрок сам выбирает место (BuildGrid, BuildGhost, BuildModeController), // а здание ставит BuildAt. Участки (BuildSlot) остались для старых сцен и сохранений -- BuildGrid их прячет. 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("[СТРОЙКА] Размещение отменено."); } // ===== Свободная стройка на сетке ===== // Данные здания по имени (имя хранится в PlacedBuildingId и в сохранении) public BuildingData FindData(string buildingName) { if (allBuildings == null || string.IsNullOrEmpty(buildingName)) return null; foreach (var b in allBuildings) if (b != null && b.buildingName == buildingName) return b; return null; } // Можно ли поставить здание на клетки (левая нижняя cell): клетки свободны и ресурсов хватает public bool CanBuildAt(BuildingData data, Vector2Int cell) { if (data == null || data.prefab == null) return false; return BuildGrid.Instance.CanPlace(cell, BuildFootprint.Of(data).size) && data.CanAfford(); } // Построить здание на клетках сетки. Возвращает здание или null (занято / мало ресурсов). public GameObject BuildAt(BuildingData data, Vector2Int cell) { var grid = BuildGrid.Instance; grid.Refresh(); // занятость на этот момент, а не 0.3 с назад if (!CanBuildAt(data, cell)) return null; long buildMs = BuildTimer.DurationMs(false, 1); if (buildMs > 0 && BuildTimer.FreeBuilders == 0) return null; // все строители заняты data.PayCost(); var fp = BuildFootprint.Of(data); Vector2 center = grid.RectCenter(cell, fp.size); Vector3 pos = new Vector3(center.x - fp.anchorOffset.x, center.y - fp.anchorOffset.y, 0f); GameObject built = Instantiate(data.prefab, pos, Quaternion.identity); if (!built.activeSelf) built.SetActive(true); // шаблон казарм хранится выключенным built.AddComponent().buildingName = data.buildingName; if (buildMs > 0) { long now = GameClock.UnixNowMs; BuildTimer.Begin(built, BuildTimer.Kind.Build, 1, now, now + buildMs); // стоит, но не работает, пока строится } Debug.Log($"[СТРОЙКА] Построено: {data.buildingName}, клетки {cell} размер {fp.size}"); EventBus.Publish(new BuildingPlacedEvent { BuildingName = data.buildingName }); grid.Refresh(); return built; } // ===== Старая схема: тап по участку -> выбор здания (участки спрятаны, оставлено для совместимости) ===== // Построить здание сразу на указанном участке. 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); } }