248 lines
10 KiB
C#
248 lines
10 KiB
C#
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<BuildingData> 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<BuildSlot>();
|
|
}
|
|
|
|
// Список зданий, доступных для стройки ПРЯМО СЕЙЧАС (по уровню Ратуши)
|
|
public List<BuildingData> GetAvailableBuildings()
|
|
{
|
|
List<BuildingData> available = new List<BuildingData>();
|
|
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<PlacedBuildingId>();
|
|
idMarker.buildingName = currentlyPlacing.buildingName;
|
|
|
|
slot.Occupy(built);
|
|
|
|
BuildingSlotLink link = built.AddComponent<BuildingSlotLink>();
|
|
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<PlacedBuildingId>().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<BuildSlot>(FindObjectsSortMode.None))
|
|
{
|
|
var col = slot.GetComponent<Collider2D>();
|
|
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<BuildSlot>(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);
|
|
}
|
|
}
|
|
|