Files
MVP1/Assets/Scripts/Modules/Settlement/SettlementModule.cs
T

228 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// Стройка или улучшение, которое ещё идёт (01.11): 0 -- нет, 1 -- постройка, 2 -- улучшение
public int timerKind;
public long timerStartMs;
public long timerEndMs;
public int timerTargetLevel;
}
// Имена полей совпадают со старым save.json -- старые сохранения читаются без переделки
[Serializable]
public class SettlementSaveData
{
public int townHallLevel = 1;
// Улучшение Ратуши, которое ещё идёт (01.11): timerEndMs 0 -- нет
public long townHallTimerStartMs;
public long townHallTimerEndMs;
public int townHallTimerTargetLevel;
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;
var thTimer = BuildTimer.Of(TownHall.Instance.gameObject);
if (thTimer != null && BuildTimer.All.Contains(thTimer))
{
data.townHallTimerStartMs = thTimer.startMs;
data.townHallTimerEndMs = thTimer.endMs;
data.townHallTimerTargetLevel = thTimer.targetLevel;
}
}
foreach (var placed in UnityEngine.Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
{
if (string.IsNullOrEmpty(placed.buildingName)) continue; // уже убрано, Destroy сработает в конце кадра
var leveled = placed.GetComponent<ILeveledBuilding>();
var timer = placed.GetComponent<BuildTimer>();
bool timerOn = timer != null && BuildTimer.All.Contains(timer);
data.placedBuildings.Add(new PlacedBuildingSaveEntry
{
buildingName = placed.buildingName,
posX = placed.transform.position.x,
posY = placed.transform.position.y,
level = leveled != null ? leveled.Level : 1,
timerKind = timerOn ? (int)timer.kind : 0,
timerStartMs = timerOn ? timer.startMs : 0,
timerEndMs = timerOn ? timer.endMs : 0,
timerTargetLevel = timerOn ? timer.targetLevel : 0
});
}
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;
var oldTimer = BuildTimer.Of(TownHall.Instance.gameObject);
if (oldTimer != null) oldTimer.Cancel();
if (cached.townHallTimerEndMs > 0)
BuildTimer.Begin(TownHall.Instance.gameObject, BuildTimer.Kind.Upgrade, cached.townHallTimerTargetLevel,
cached.townHallTimerStartMs, cached.townHallTimerEndMs);
}
// Сначала убираем уже заспавненные здания, чтобы не задвоить постройки
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;
// Стройка или улучшение продолжается (если время уже вышло -- закончится в первом же кадре)
if (entry.timerKind == (int)BuildTimer.Kind.Build || entry.timerKind == (int)BuildTimer.Kind.Upgrade)
BuildTimer.Begin(built, (BuildTimer.Kind)entry.timerKind, Mathf.Max(1, entry.timerTargetLevel),
entry.timerStartMs, entry.timerEndMs);
// Помечаем слот занятым, чтобы поверх нельзя было поставить ещё одно здание
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; // чтобы сохранение в этом же кадре уже не видело здание
var timer = e.GetComponent<BuildTimer>();
if (timer != null) timer.Discard(); // строитель свободен сразу, а не в конце кадра
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;
var thTimer = BuildTimer.Of(TownHall.Instance.gameObject);
if (thTimer != null) thTimer.Cancel();
}
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);
}
}
}