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 placedBuildings = new List(); } 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(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(FindObjectsSortMode.None)) { if (string.IsNullOrEmpty(placed.buildingName)) continue; // уже убрано, Destroy сработает в конце кадра var leveled = placed.GetComponent(); 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().buildingName = entry.buildingName; var leveled = built.GetComponent(); if (leveled != null) leveled.Level = entry.level; // Помечаем слот занятым, чтобы поверх нельзя было поставить ещё одно здание BuildSlot slot = BuildManager.Instance.FindSlotAtPosition(pos); if (slot != null) { slot.Occupy(built); built.AddComponent().linkedSlot = slot; } restored++; } Debug.Log($"[ПОСЕЛЕНИЕ] Город восстановлен. Построек: {restored}"); } // Убрать все постройки со сцены. Участок освобождаем сразу: Destroy срабатывает в конце кадра, // и иначе старое здание освободило бы участок, на который уже встало новое static void ClearPlacedBuildings() { foreach (var e in UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None)) { var link = e.GetComponent(); 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(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(json) ?? new SettlementSaveData(); if (cached.placedBuildings == null) cached.placedBuildings = new List(); ApplyToScene(force: true); } } }