183 lines
8.2 KiB
C#
183 lines
8.2 KiB
C#
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|