using UnityEngine; // ТОЧКА СБОРКИ МОДУЛЕЙ (слой приложения). Документ: Docs/Modules/README.md // // Здесь и только здесь модули соединяются с остальной игрой: // 1. регистрируются реализации контрактов, которые не регистрируются сами; // 2. события модулей подключаются к системам, которые ещё не разнесены по модулям (GameProgress, Казармы). // Модули друг о друге ничего не знают -- всю "проводку" видно в одном файле. public static class GameModules { // Модули экономики для слоя приложения (окна, синхронизация города). Модули друг друга так не берут -- только через контракты. public static PopulationService Population { get; private set; } public static ProductionService Production { get; private set; } // После SubsystemRegistration (там ядро очищается) и раньше любых Awake [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] static void Register() { // Боевая система -- чистый C#, сама зарегистрироваться не может. // Числа боя -- из таблицы BattleConfig, сила воина каждого типа -- из GameData.Troops var unitPower = new int[TroopType.Count]; for (int i = 0; i < TroopType.Count && i < GameData.Troops.Count; i++) unitPower[i] = GameData.Troops[i].power; GameServices.Register(new BattleSystem(BattleConfig.Load().ToRules(unitPower))); // Марши: скорости по типам -- в модуль Мира, слоты похода -- в модуль Армии (таблица MarchConfig) var marchConfig = MarchConfig.Load(); MarchManager.SpeedRules = marchConfig.ToSpeedRules(); MarchManager.PointRules = WorldResourceConfig.Load().ToRules(); // ресурсные точки (регламент 31.10) MarchManager.Patrols = marchConfig.ToPatrolRules(); // патрули-заглушки соседей (Разведка М1) // Разведка -- чистый C#: туман, обзор, чужие армии (секция "intelligence"). Карту и армии читает у Мира // через IWorldMap; досчитывает наблюдение перед каждым расчётом мира (WorldAdvancingEvent) var intelConfig = IntelligenceConfig.Load(); var intelligence = new IntelligenceService(intelConfig.ToRules()); GameServices.Register(intelligence); SaveRegistry.Register(intelligence); EventBus.Subscribe(e => intelligence.ObserveUntil(e.ToMs)); // «Обзор» для теста, пока нет дерева технологий (после загрузки и в новой игре) void TestObservation() { if (intelConfig.testObservationLevel > 0) intelligence.ObservationLevel = intelConfig.testObservationLevel; } TestObservation(); EventBus.Subscribe(_ => TestObservation()); EventBus.Subscribe(_ => TestObservation()); // Армия -- чистый C#, единственное место хранения войск (секция "army") var army = new ArmyService(marchConfig.baseMarchSlots, HospitalConfig.Load().ToRules()); // госпиталь (решения 31.10, п. 7) GameServices.Register(army); SaveRegistry.Register(army); EventBus.Subscribe(_ => { if (GameProgress.HasInstance) GameProgress.Instance.MarkChanged(); }); EventBus.Subscribe(_ => MigrateLegacyArmy(army)); // Очередь обучения -- у Армии (решения 31.10, п. 8): статистика заданий по её событию, старая очередь -- переносится EventBus.Subscribe(e => { if (GameProgress.HasInstance) GameProgress.Instance.OnTroopsTrained(e); }); EventBus.Subscribe(_ => MigrateLegacyTraining(army)); // Раненые вылечены -- вернулись в город EventBus.Subscribe(e => { if (e.Type >= 0 && e.Type < GameData.Troops.Count) UIKit.Toast("Вылечены в госпитале: " + e.Count + " × " + GameData.Troops[e.Type].name); if (GameProgress.HasInstance) GameProgress.Instance.MarkChanged(); }); // Население -- чистый C#: жители, рабочие, еда, прирост (секция "population"). Числа -- из EconomyConfig. var config = EconomyConfig.Load(); var population = new PopulationService(ToPopulationRules(config)); Population = population; GameServices.Register(population); SaveRegistry.Register(population); // Добыча через рабочих -- чистый C# (секция "production"). Какие здания стоят, сообщает App/CitySync. var production = new ProductionService(ToProductionRules(config)); Production = production; GameServices.Register(production); SaveRegistry.Register(production); // Склад и Амбар появляются в списке стройки кодом -- до восстановления зданий из сохранения EventBus.Subscribe(_ => StorageBootstrap.Ensure()); // Тап по зданию -> окно здания и рабочих (статические события переживают Play в редакторе -- сначала отписка) ResourceCollector.Tapped -= BuildingWindow.OnWorkplaceTapped; ResourceCollector.Tapped += BuildingWindow.OnWorkplaceTapped; StorageBuilding.Tapped -= BuildingWindow.OnStorageTapped; StorageBuilding.Tapped += BuildingWindow.OnStorageTapped; TownHall.Tapped -= BuildingWindow.OnTownHallTapped; TownHall.Tapped += BuildingWindow.OnTownHallTapped; EventBus.Subscribe(_ => population.OnGameLoaded(GameClock.UnixNow)); // офлайн-прогресс EventBus.Subscribe(e => population.Tick(e.UnixNow)); // перемотка (отладка) EventBus.Subscribe(_ => population.Changed()); // воины -- тоже люди EventBus.Subscribe(_ => population.Changed()); // предел жилья вырос EventBus.Subscribe(SyncPopulationResource); EventBus.Subscribe(_ => GiveStartingKit(population, army, config)); EventBus.Subscribe(_ => GiveStartingKit(population, army, config)); // Круг -- чистый C#, сам сохраняет, в каком Круге игрок (секция "circle") var circle = new CircleService(); GameServices.Register(circle); SaveRegistry.Register(circle); // Чат -- чистый C#; история живёт, пока открыта игра (между сценами тоже) GameServices.Register(new ChatService(System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())); // Казармы появляются в списке стройки кодом -- до восстановления зданий из сохранения EventBus.Subscribe(_ => BarracksBootstrap.Ensure()); // Госпиталь (решения 31.10, п. 7): здание города даёт места для лечения Армии через контракт IHospital GameServices.Register(new HospitalBeds()); EventBus.Subscribe(_ => HospitalBootstrap.Ensure()); // Статистика для заданий (GameProgress) -- по событиям Мира EventBus.Subscribe(OnMarchStarted); EventBus.Subscribe(OnMarchReturned); // Окно "ВОЙСКО ВЕРНУЛОСЬ" в городе MarchReturnPopup.Reset(); EventBus.Subscribe(MarchReturnPopup.OnMarchReturned); } // Сцена загружена, Awake всех объектов прошли, Start -- ещё нет (сохранение грузится в Start) [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void MigrateLegacyMarches() { // Раньше походы хранились в progress.json (GameProgress.Data.sashaMarches). // Теперь их сохраняет модуль Мира в save.json, секция "world". Переносим один раз. var data = GameProgress.Instance.Data; if (data.sashaMarches == null || data.sashaMarches.Count == 0 || MarchManager.Instance == null) return; // Если в save.json уже есть секция "world", загрузка сохранения заменит эти походы на сохранённые MarchManager.Instance.LoadMarches(new System.Collections.Generic.List(data.sashaMarches)); // Из progress.json убираем, только когда походы уже записаны в save.json -- иначе вылет игры их потеряет EventBus.Subscribe(ClearLegacyMarches); } static void ClearLegacyMarches(GameSavedEvent e) { EventBus.Unsubscribe(ClearLegacyMarches); GameProgress.Instance.Data.sashaMarches.Clear(); GameProgress.Instance.MarkChanged(); Debug.Log("[МОДУЛИ] Походы перенесены из progress.json в модуль Мира."); } // Раньше войска хранились в progress.json (GameProgress.Data.troops и sashaMarchUnits моста ArmyBridge). // Теперь -- в модуле Армии, save.json, секция "army". Переносим один раз, после загрузки сохранения. static void MigrateLegacyArmy(ArmyService army) { var data = GameProgress.Instance.Data; if (data.armyInModule) return; if (!army.RestoredFromSave) { var legacyMarches = new System.Collections.Generic.List>(); foreach (var m in data.sashaMarchUnits) legacyMarches.Add(new System.Collections.Generic.KeyValuePair(m.marchId, m.units)); army.ImportLegacy(data.troops, legacyMarches); Debug.Log($"[МОДУЛИ] Войска перенесены из progress.json в модуль Армии: в городе {army.SoldiersAtHome}, в походах {army.TroopsInMarches}."); } // Из progress.json убираем, только когда войска уже записаны в save.json -- иначе вылет игры их потеряет if (army.RestoredFromSave) ClearLegacyArmy(default); else EventBus.Subscribe(ClearLegacyArmy); } static void ClearLegacyArmy(GameSavedEvent e) { EventBus.Unsubscribe(ClearLegacyArmy); var data = GameProgress.Instance.Data; data.armyInModule = true; System.Array.Clear(data.troops, 0, data.troops.Length); data.sashaMarchUnits.Clear(); GameProgress.Instance.MarkChanged(); } // Очередь обучения раньше вёл GameProgress (progress.json, Data.training, время -- тики реальных часов). // Теперь -- Армия (save.json, "army", мс часов игры). Переносим один раз, с тем же оставшимся временем. static void MigrateLegacyTraining(ArmyService army) { var data = GameProgress.Instance.Data; if (data.training == null || data.training.Count == 0) return; // В Армии уже есть заказы -- перенос уже был (сохранение записалось, а progress.json -- ещё нет): только убрать старое if (army.Training.Count == 0) { long epochTicks = new System.DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc).Ticks; long offsetMs = GameClock.OffsetSeconds * 1000; // старые тики -- реальное время, Армия -- часы игры var orders = new System.Collections.Generic.List<(int, int, long, long, bool)>(); foreach (var o in data.training) orders.Add((o.troopIndex, o.count, (o.startTicks - epochTicks) / 10_000 + offsetMs, (o.endTicks - epochTicks) / 10_000 + offsetMs, o.recruited)); army.ImportLegacyTraining(orders); Debug.Log($"[МОДУЛИ] Очередь обучения перенесена из progress.json в модуль Армии: заказов {orders.Count}."); } // Из progress.json убираем, только когда очередь уже записана в save.json -- иначе вылет игры её потеряет EventBus.Subscribe(ClearLegacyTraining); } static void ClearLegacyTraining(GameSavedEvent e) { EventBus.Unsubscribe(ClearLegacyTraining); GameProgress.Instance.Data.training.Clear(); GameProgress.Instance.MarkChanged(); } // ---------------- Население и экономика ---------------- static PopulationRules ToPopulationRules(EconomyConfig config) { var p = config.population; return new PopulationRules { StartTotal = p.startTotal, StartWorkers = p.startWorkers, StartSoldiers = p.startSoldiers, FoodPerCivilianPerHour = p.foodPerCivilianPerHour, FoodPerSoldierPerHour = p.foodPerSoldierPerHour, GrowthPerHour = p.growthPerHour, HousingBase = p.housingBase, HousingPerTownHallLevel = p.housingPerTownHallLevel, HousingPerHouseLevel = p.housingPerHouseLevel, MaxOfflineHours = p.maxOfflineHours, CivilianReserveByTownHallLevel = p.civilianReserveByTownHallLevel != null && p.civilianReserveByTownHallLevel.Length > 0 ? (int[])p.civilianReserveByTownHallLevel.Clone() : new PopulationRules().CivilianReserveByTownHallLevel, PeoplePerSoldier = Mathf.Max(1, config.soldier.people) }; } static ProductionRules ToProductionRules(EconomyConfig config) { var p = config.production; return new ProductionRules { // порядок как в WorkJob: лесоруб, фермер, каменотёс, шахтёр-металл, шахтёр-золото PerWorkerPerHour = new double[] { p.woodcutterPerHour, p.farmerPerHour, p.stonecutterPerHour, p.minerIronPerHour, p.minerGoldPerHour }, SlotsPerBuilding = p.workerSlotsT1, SlotsPerLevel = p.workerSlotsPerLevel, EfficiencyPerLevel = p.efficiencyPerLevel, FamineNonFoodMultiplier = p.famineNonFoodMultiplier }; } // Новая игра: стартовое войско (пехота) и стартовые ресурсы из EconomyConfig. Один раз на новую игру. static void GiveStartingKit(PopulationService population, ArmyService army, EconomyConfig config) { if (!population.TryTakeStartingKit()) return; if (population.Rules.StartSoldiers > 0) army.AddTroops(0, population.Rules.StartSoldiers); if (GameServices.TryGet(out IEconomy economy)) { ResourceType[] types = { ResourceType.Wood, ResourceType.Food, ResourceType.Stone, ResourceType.Iron, ResourceType.Gold, ResourceType.Gems }; foreach (var t in types) { float target = config.GetStartResource(t); float current = economy.Get(t); if (target > current) economy.Add(t, target - current); // не меньше стартового } } Debug.Log($"[НАСЕЛЕНИЕ] Новая игра: {population.Total} человек, воинов {population.Soldiers}."); } // Верхняя панель показывает людей как ресурс "Люди" -- держим его равным населению static void SyncPopulationResource(PopulationChangedEvent e) { if (!GameServices.TryGet(out IEconomy economy)) return; float delta = e.Total - economy.Get(ResourceType.Population); if (Mathf.Abs(delta) >= 0.5f) economy.Add(ResourceType.Population, delta); } static void OnMarchStarted(MarchStartedEvent e) { var data = GameProgress.Instance.Data; data.marchesSent++; data.dailyMarches++; GameProgress.Instance.MarkChanged(); } static void OnMarchReturned(MarchReturnedEvent e) { if (!e.Victory) return; GameProgress.Instance.Data.worldWins++; GameProgress.Instance.MarkChanged(); } }