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#, сама зарегистрироваться не может // Сила воина каждого типа -- из 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(unitPower)); // Армия -- чистый C#, единственное место хранения войск (секция "army") var army = new ArmyService(); GameServices.Register(army); SaveRegistry.Register(army); EventBus.Subscribe(_ => { if (GameProgress.HasInstance) GameProgress.Instance.MarkChanged(); }); EventBus.Subscribe(_ => MigrateLegacyArmy(army)); // Население -- чистый 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; 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()); // Статистика для заданий (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(); } // ---------------- Население и экономика ---------------- 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 }; } 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 }; } // Новая игра: стартовое войско (пехота) и стартовые ресурсы из 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(); } }