Files
MVP1/Assets/Scripts/Modules/Settlement/SettlementModule.cs
T
AlexanderandClaude Opus 5.5 964cb23d9d Модульная система: игровое ядро + 6 модулей, документы Docs/Modules
- Ядро (Bogatyri.Core): контракты модулей, EventBus, GameServices, SaveRegistry
- Модули: Экономика, Поселение, Армия, Глобальный мир, Боевая система, Сохранения
  (каждый — отдельный asmdef, видит только ядро)
- App/GameModules: регистрация модулей и связь с GameProgress/Казармами
- Сохранения v2 по секциям модулей, перенос старого save.json и походов из progress.json
- Сброс сохранений: ISaveSystem.ResetAll, ResetState у модулей, меню Tools
- EditMode-тесты модулей, документы по каждому модулю с журналом изменений

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:39:28 +02:00

183 lines
8.2 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;
}
// Имена полей совпадают со старым 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);
}
}
}