Files

183 lines
8.2 KiB
C#
Raw Permalink Normal View History

using System;
using System.Collections.Generic;
using UnityEngine;
// МОДУЛЬ ПОСЕЛЕНИЯ: точка входа. Реализует ISettlement и сам сохраняет город (секция "settlement").
// Документ: Docs/Modules/Settlement.md
//
// Город живёт на сцене MVP1, а сохраняться игра может и на карте мира. Поэтому модуль держит
// последнее известное состояние города в памяти: на карте мира сохраняется оно, а не пустая сцена.
public static class SettlementModule
{
[Serializable]
public class PlacedBuildingSaveEntry
{
public string buildingName; // совпадает с BuildingData.buildingName
public float posX;
public float posY;
public int level;
}
// Имена полей совпадают со старым save.json -- старые сохранения читаются без переделки
[Serializable]
public class SettlementSaveData
{
public int townHallLevel = 1;
public List<PlacedBuildingSaveEntry> placedBuildings = new List<PlacedBuildingSaveEntry>();
}
static SettlementSaveData cached; // последнее состояние города (из сцены или из файла)
static BuildManager appliedToBuildManager; // BuildManager сцены, для которой город уже восстановлен
static readonly Service service = new Service();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void Init()
{
cached = null;
appliedToBuildManager = null;
GameServices.Register<ISettlement>(service);
SaveRegistry.Register(service);
}
static bool CityLoaded => BuildManager.Instance != null;
// ---------------- Сцена города <-> данные ----------------
static SettlementSaveData CaptureScene()
{
var data = new SettlementSaveData();
if (TownHall.Instance != null) data.townHallLevel = TownHall.Instance.currentLevel;
foreach (var placed in UnityEngine.Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
{
if (string.IsNullOrEmpty(placed.buildingName)) continue; // уже убрано, Destroy сработает в конце кадра
var leveled = placed.GetComponent<ILeveledBuilding>();
data.placedBuildings.Add(new PlacedBuildingSaveEntry
{
buildingName = placed.buildingName,
posX = placed.transform.position.x,
posY = placed.transform.position.y,
level = leveled != null ? leveled.Level : 1
});
}
return data;
}
// Восстановить город на сцене из последнего известного состояния.
// Без force -- один раз на каждую загрузку сцены города (BuildManager.Start и загрузка сохранения
// могут прийти в любом порядке, повторное восстановление не нужно).
public static void ApplyToScene(bool force = false)
{
if (cached == null || !CityLoaded) return;
if (!force && appliedToBuildManager == BuildManager.Instance) return;
appliedToBuildManager = BuildManager.Instance;
EventBus.Publish(new SettlementRestoringEvent()); // например, Казармы должны попасть в список стройки
if (TownHall.Instance != null) TownHall.Instance.currentLevel = cached.townHallLevel;
// Сначала убираем уже заспавненные здания, чтобы не задвоить постройки
ClearPlacedBuildings();
int restored = 0;
foreach (var entry in cached.placedBuildings)
{
BuildingData matchingData = FindBuildingDataByName(entry.buildingName);
if (matchingData == null)
{
Debug.LogWarning($"[ПОСЕЛЕНИЕ] Не найден BuildingData для '{entry.buildingName}', здание пропущено.");
continue;
}
Vector3 pos = new Vector3(entry.posX, entry.posY, 0f);
GameObject built = UnityEngine.Object.Instantiate(matchingData.prefab, pos, Quaternion.identity);
if (!built.activeSelf) built.SetActive(true); // шаблон казарм хранится выключенным
built.AddComponent<PlacedBuildingId>().buildingName = entry.buildingName;
var leveled = built.GetComponent<ILeveledBuilding>();
if (leveled != null) leveled.Level = entry.level;
// Помечаем слот занятым, чтобы поверх нельзя было поставить ещё одно здание
BuildSlot slot = BuildManager.Instance.FindSlotAtPosition(pos);
if (slot != null)
{
slot.Occupy(built);
built.AddComponent<BuildingSlotLink>().linkedSlot = slot;
}
restored++;
}
Debug.Log($"[ПОСЕЛЕНИЕ] Город восстановлен. Построек: {restored}");
}
// Убрать все постройки со сцены. Участок освобождаем сразу: Destroy срабатывает в конце кадра,
// и иначе старое здание освободило бы участок, на который уже встало новое
static void ClearPlacedBuildings()
{
foreach (var e in UnityEngine.Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
{
var link = e.GetComponent<BuildingSlotLink>();
if (link != null && link.linkedSlot != null)
{
link.linkedSlot.Free();
link.linkedSlot = null;
}
e.buildingName = null; // чтобы сохранение в этом же кадре уже не видело здание
UnityEngine.Object.Destroy(e.gameObject);
}
}
static BuildingData FindBuildingDataByName(string buildingName)
{
foreach (var b in BuildManager.Instance.allBuildings)
{
if (b != null && b.buildingName == buildingName) return b;
}
return null;
}
// ---------------- Контракты ----------------
class Service : ISettlement, ISaveParticipant
{
public int TownHallLevel =>
TownHall.Instance != null ? TownHall.Instance.currentLevel : (cached?.townHallLevel ?? 1);
public int BuildingCount =>
CityLoaded
? UnityEngine.Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None).Length
: (cached?.placedBuildings.Count ?? 0);
public bool IsCityLoaded => CityLoaded;
public string SaveKey => "settlement";
public string Capture()
{
if (CityLoaded) cached = CaptureScene();
// Город не открыт и ни разу не загружался -- не трогаем то, что уже лежит в файле
return cached != null ? JsonUtility.ToJson(cached) : null;
}
// Новая игра: Ратуша 1 уровня, построек нет -- и на сцене, и в памяти модуля
public void ResetState()
{
cached = new SettlementSaveData();
if (!CityLoaded) return;
ClearPlacedBuildings();
if (TownHall.Instance != null) TownHall.Instance.currentLevel = 1;
appliedToBuildManager = BuildManager.Instance; // город уже в состоянии новой игры
EventBus.Publish(new TownHallLevelChangedEvent { Level = 1 });
}
public void Restore(string json)
{
cached = JsonUtility.FromJson<SettlementSaveData>(json) ?? new SettlementSaveData();
if (cached.placedBuildings == null) cached.placedBuildings = new List<PlacedBuildingSaveEntry>();
ApplyToScene(force: true);
}
}
}