using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; // Окно здания. Открывается тапом по зданию (подписей над зданиями больше нет -- всё здесь). // У каждого здания своё окно: // - добывающее (Лесопилка, Ферма, Каменоломня, Шахта, Рудник): рабочие этой профессии и кнопки "−" / "+", // добыча в час, сколько ресурса на складе и когда склад заполнится, что даст улучшение и его цена; // - Склад и Амбар: запас / лимит по их ресурсам, что даст улучшение и цена; // - Дом: сколько жилья даёт, население / предел, улучшение и цена; // - Ратуша: жильё, резерв для найма, что откроется на следующем уровне, цена улучшения; // - Кристалл: где добывается, запас / лимит; // - Казармы: очередь обучения, какие войска открыты и что нужно для остальных, улучшение (открывает войска), // кнопка "Нанять войска" (панель "Войска"). // Без здания (кнопка "Рабочие" в панели DEV и "Все рабочие" в окне) -- список рабочих по всем профессиям. // Окно -- только интерфейс: рабочих ставит модуль Населения, добычу считает модуль Экономики. // Пока окно открыто, цифры обновляются сами (раз в полсекунды). public static class BuildingWindow { static GameObject root; static GameObject focus; // здание, по которому тапнули (null -- список рабочих) static readonly List live = new List(); // что обновлять, пока окно открыто public static void OnWorkplaceTapped(ResourceCollector building) { Open(building != null ? building.gameObject : null); } public static void OnStorageTapped(StorageBuilding building) { Open(building != null ? building.gameObject : null); } public static void OnTownHallTapped(TownHall townHall) { Open(townHall != null ? townHall.gameObject : null); } public static void Open(GameObject building) { // В режиме стройки тап -- это выбор места, а не здания if (BuildModeController.Instance != null && BuildModeController.Instance.IsActive) return; focus = building; Rebuild(); } public static void Close() { if (root != null) UnityEngine.Object.Destroy(root); root = null; focus = null; live.Clear(); } static void Rebuild() { if (root != null) UnityEngine.Object.Destroy(root); live.Clear(); var card = UIKit.Modal(TitleFor(focus), out root); root.AddComponent(); var rc = focus != null ? focus.GetComponent() : null; var storage = focus != null ? focus.GetComponent() : null; var townHall = focus != null ? focus.GetComponent() : null; var barracks = focus != null ? focus.GetComponent() : null; if (focus == null) WorkersList(card); else if (townHall != null) TownHallSection(card, townHall); else if (barracks != null) BarracksSection(card, barracks); else if (rc != null && rc.IsWorkplace) WorkplaceSection(card, rc); else if (rc != null && rc.IsHouse) HouseSection(card, rc); else if (rc != null && rc.resourceType == ResourceType.Gems) CrystalSection(card); else if (storage != null) StorageSection(card, storage); UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, Close, -1, 80, 28); } internal static void Tick() { if (root == null) return; if (focus == null && FocusWasDestroyed) { Close(); return; } foreach (var update in live) update(); } static bool focusSet; static bool FocusWasDestroyed => focusSet && focus == null; static string TitleFor(GameObject building) { focusSet = building != null; if (building == null) return "РАБОЧИЕ"; var townHall = building.GetComponent(); if (townHall != null) return $"РАТУША — {townHall.currentLevel} УР."; var id = building.GetComponent(); var leveled = building.GetComponent(); string name = id != null && !string.IsNullOrEmpty(id.buildingName) ? id.buildingName : building.name; return leveled != null ? $"{name.ToUpper()} — {leveled.Level} УР." : name.ToUpper(); } // ---------------- Добывающее здание ---------------- static void WorkplaceSection(RectTransform card, ResourceCollector rc) { if (!WorkJobs.TryFromResource(rc.resourceType, out WorkJob job)) return; var production = GameModules.Production; if (production == null || !GameServices.TryGet(out IPopulation population) || !GameServices.TryGet(out IEconomy economy)) { UIKit.Text(card, "Модули Населения и Экономики не подключены", 26, UIKit.Theme.Red, TextAlignmentOptions.Center); return; } ResourceType res = WorkJobs.Resource(job); string resName = GameData.ResName(res).ToLower(); double norm = production.Rules.PerWorkerPerHour[(int)job]; // Что даёт это здание int slots = production.BuildingSlots(rc.level); double eff = production.BuildingEfficiency(rc.level); UIKit.Text(card, $"Здание даёт {slots} мест, норма ×{eff:0.##} — до {slots * norm * eff:0.#} {resName} в час при полном штате.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); // Рабочие профессии и кнопки Header(card, $"Рабочие: {WorkJobs.Name(job).ToLower()}"); Live(card, () => $"{population.GetWorkers(job)} / {production.Slots(job)} мест в городе • свободных жителей: {population.FreeWorkers}", 26, UIKit.Theme.Text); var row = UIKit.Row(card, 72, 8); StepButton(row, "−5", job, -5); StepButton(row, "−1", job, -1); StepButton(row, "+1", job, +1); StepButton(row, "+5", job, +5); // Добыча и склад Header(card, "Добыча и склад"); Live(card, () => { float rate = production.RatePerHour(res); string famine = population.IsStarving ? " голод: добыча ×0.5" : ""; return $"Сейчас: +{rate:0.#} {resName} в час{famine}"; }, 26, UIKit.Theme.Text); StorageBar(card, economy, res); Live(card, () => FillTimeText(economy, res, production.RatePerHour(res)), 22, UIKit.Theme.TextDim); // Улучшение int nextSlots = production.BuildingSlots(rc.level + 1); double nextEff = production.BuildingEfficiency(rc.level + 1); UpgradeBlock(card, rc.level, $"мест {slots} → {nextSlots}, норма ×{eff:0.##} → ×{nextEff:0.##}", () => rc.Level = rc.level + 1); UIKit.Button(card, "Все рабочие", UIKit.Theme.Blue, () => Open(null), -1, 70, 24); } // ---------------- Дом ---------------- static void HouseSection(RectTransform card, ResourceCollector house) { int perLevel = EconomyConfig.Load().population.housingPerHouseLevel; UIKit.Text(card, $"Дом даёт +{perLevel * house.level} к пределу жилья (+{perLevel} за уровень).", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (GameServices.TryGet(out IPopulation population)) { Header(card, "Население"); LiveBar(card, () => population.HousingCap > 0 ? (float)population.Total / population.HousingCap : 0f, () => $"{population.Total} / {population.HousingCap} человек", UIKit.Theme.Blue); Live(card, () => population.Total >= population.HousingCap ? "Предел жилья: прирост стоит, нужны Дома или Ратуша выше" : $"Прирост: +{population.GrowthPerHour:0.#} человека в час", 22, UIKit.Theme.TextDim); } UpgradeBlock(card, house.level, $"жильё +{perLevel * house.level} → +{perLevel * (house.level + 1)}", () => house.Level = house.level + 1); } // ---------------- Кристалл ---------------- static void CrystalSection(RectTransform card) { UIKit.Text(card, "Синий кристалл добывается в кристальных разломах на карте мира. Здание не улучшается.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (GameServices.TryGet(out IEconomy economy)) StorageBar(card, economy, ResourceType.Gems); } // ---------------- Склад и Амбар ---------------- static void StorageSection(RectTransform card, StorageBuilding storage) { var s = EconomyConfig.Load().storage; bool warehouse = storage.kind == StorageBuilding.Kind.Warehouse; float perLevel = warehouse ? s.perWarehouseLevel : s.perGranaryLevel; UIKit.Text(card, warehouse ? $"Склад даёт +{perLevel * storage.level:0} к лимиту дерева, камня, металла и золота." : $"Амбар даёт +{perLevel * storage.level:0} к лимиту еды.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (GameServices.TryGet(out IEconomy economy)) { Header(card, "Запас / лимит"); if (warehouse) { StorageBar(card, economy, ResourceType.Wood); StorageBar(card, economy, ResourceType.Stone); StorageBar(card, economy, ResourceType.Iron); StorageBar(card, economy, ResourceType.Gold); } else StorageBar(card, economy, ResourceType.Food); UIKit.Text(card, "Сверх лимита ресурс не прибавляется — пропадает.", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); } UpgradeBlock(card, storage.level, $"лимит +{perLevel * storage.level:0} → +{perLevel * (storage.level + 1):0}", () => storage.Level = storage.level + 1); } // ---------------- Ратуша ---------------- static void TownHallSection(RectTransform card, TownHall townHall) { var config = EconomyConfig.Load(); int level = townHall.currentLevel; int next = level + 1; var p = config.population; UIKit.Text(card, $"Ратуша даёт {p.housingBase + p.housingPerTownHallLevel * (level - 1)} мест жилья. От её уровня зависят резерв для найма и доступные здания и войска.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (GameServices.TryGet(out IPopulation population)) { Header(card, "Город"); LiveBar(card, () => population.HousingCap > 0 ? (float)population.Total / population.HousingCap : 0f, () => $"Население {population.Total} / {population.HousingCap}", UIKit.Theme.Blue); Live(card, () => $"Жителей {population.Civilians}: рабочих {population.Workers}, свободных {population.Idle} • воинов {population.Soldiers}", 22, UIKit.Theme.TextDim); Live(card, () => $"Резерв для найма: {population.CivilianReserve} • можно нанять: {population.Recruitable}", 22, UIKit.Theme.TextDim); } if (level >= townHall.maxLevel) { UIKit.Text(card, "Максимальный уровень", 26, UIKit.Theme.Gold, TextAlignmentOptions.Center); return; } // Что даст следующий уровень Header(card, $"Уровень {next}"); var gains = new List { $"жильё +{p.housingPerTownHallLevel}" }; int nowReserve = ReserveAt(level), nextReserve = ReserveAt(next); if (nextReserve != nowReserve) gains.Add($"резерв для найма {nowReserve} → {nextReserve}"); UIKit.Text(card, string.Join(", ", gains.ToArray()), 24, UIKit.Theme.Text, TextAlignmentOptions.Center); string unlocks = UnlocksAt(next); if (unlocks.Length > 0) UIKit.Text(card, "Откроется: " + unlocks, 24, UIKit.Theme.Gold, TextAlignmentOptions.Center); var cost = BuildingData.ToAmounts(townHall.GetUpgradeCost()); UIKit.Text(card, "Цена: " + CostText(cost), 24, UIKit.Theme.Text, TextAlignmentOptions.Center); bool canPay = townHall.CanUpgrade(); var b = UIKit.Button(card, $"Улучшить до {next} ур.", canPay ? UIKit.Theme.Green : UIKit.Theme.Gray, () => { if (!townHall.CanUpgrade()) { UIKit.Toast("Не хватает ресурсов"); return; } townHall.Upgrade(); if (CitySync.Instance != null) CitySync.Instance.SyncNow(); UIKit.Toast($"Ратуша улучшена до {townHall.currentLevel} уровня"); Rebuild(); }, -1, 80, 26); b.interactable = canPay; } // ---------------- Казармы ---------------- static void BarracksSection(RectTransform card, Barracks barracks) { var gp = GameProgress.Instance; UIKit.Text(card, $"Казармы обучают войска. Казарм в городе: {Barracks.Count}, мест в очереди: {Barracks.QueueSize}. " + $"Новые войска открывает уровень самых прокачанных Казарм ({Barracks.MaxLevel} ур.) вместе с уровнем Ратуши.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (gp != null) { Header(card, "Обучение"); Live(card, () => { var d = gp.Data; if (d.training.Count == 0) return "Никто не обучается"; var o = d.training[0]; string more = d.training.Count > 1 ? $" (ещё заказов: {d.training.Count - 1})" : ""; return $"{GameData.Troops[o.troopIndex].name} ×{o.count} — осталось {GameData.FormatTime(gp.TrainingSecondsLeft(o))}{more}"; }, 24, UIKit.Theme.Text); if (GameServices.TryGet(out IPopulation population)) Live(card, () => $"Можно нанять из жителей: {population.Recruitable} • в Казармах: {population.InTraining}", 22, UIKit.Theme.TextDim); Header(card, "Войска"); for (int i = 0; i < GameData.Troops.Count; i++) { string lockText = gp.TroopLockText(i); UIKit.Text(card, lockText.Length == 0 ? $"{GameData.Troops[i].name} — открыты" : $"{GameData.Troops[i].name} — нужно: {lockText}", 24, lockText.Length == 0 ? UIKit.Theme.Text : UIKit.Theme.Gold, TextAlignmentOptions.Center); } } // Что откроет следующий уровень этих Казарм var opens = new List(); if (gp != null) for (int i = 0; i < GameData.Troops.Count; i++) if (gp.RequiredBarracksLevel(i) == barracks.level + 1 && Barracks.MaxLevel <= barracks.level) opens.Add(GameData.Troops[i].name); string effect = opens.Count > 0 ? $"откроются: {string.Join(", ", opens.ToArray())} (если хватает уровня Ратуши)" : "ближе к новым войскам"; UpgradeBlock(card, barracks.level, effect, () => barracks.Level = barracks.level + 1); UIKit.Button(card, "Нанять войска", UIKit.Theme.Blue, () => { Close(); if (UIPanelManager.Instance != null) UIPanelManager.Instance.Open("Troops"); }, -1, 80, 26); } // Резерв для найма на уровне Ратуши (те же числа, что у модуля Населения) static int ReserveAt(int townHallLevel) { var table = GameModules.Population != null ? GameModules.Population.Rules.CivilianReserveByTownHallLevel : null; if (table == null || table.Length == 0) return 0; return table[Mathf.Clamp(townHallLevel, 1, table.Length) - 1]; } // Здания и войска, которые открываются ровно на этом уровне Ратуши static string UnlocksAt(int townHallLevel) { var names = new List(); if (BuildManager.Instance != null && BuildManager.Instance.allBuildings != null) foreach (var b in BuildManager.Instance.allBuildings) if (b != null && b.requiredTownHallLevel == townHallLevel) names.Add(b.buildingName); foreach (var t in GameData.Troops) if (t.requiredTownHall == townHallLevel) names.Add(t.name); return string.Join(", ", names.ToArray()); } // ---------------- Улучшение (общее для зданий с уровнем, кроме Ратуши) ---------------- static void UpgradeBlock(RectTransform card, int level, string effect, Action raiseLevel) { var config = EconomyConfig.Load(); if (level >= config.upgrade.maxLevel) { UIKit.Text(card, "Максимальный уровень", 26, UIKit.Theme.Gold, TextAlignmentOptions.Center); return; } Header(card, $"Улучшение до {level + 1} ур."); UIKit.Text(card, effect, 24, UIKit.Theme.Text, TextAlignmentOptions.Center); var cost = config.UpgradeCost(level); UIKit.Text(card, "Цена: " + CostText(cost), 24, UIKit.Theme.Text, TextAlignmentOptions.Center); bool canPay = GameServices.TryGet(out IEconomy economy) && economy.CanAfford(cost); var b = UIKit.Button(card, $"Улучшить до {level + 1} ур.", canPay ? UIKit.Theme.Green : UIKit.Theme.Gray, () => { if (!GameServices.TryGet(out IEconomy e) || !e.TrySpend(cost)) { UIKit.Toast("Не хватает ресурсов"); return; } raiseLevel(); if (CitySync.Instance != null) CitySync.Instance.SyncNow(); UIKit.Toast($"Улучшено до {level + 1} уровня"); Rebuild(); }, -1, 80, 26); b.interactable = canPay; } // Цена: чего хватает -- зелёным, чего нет -- красным, со сколько есть static string CostText(ResourceAmount[] cost) { GameServices.TryGet(out IEconomy economy); var parts = new string[cost.Length]; for (int i = 0; i < cost.Length; i++) { float have = economy != null ? economy.Get(cost[i].Type) : 0f; bool ok = have >= cost[i].Amount; string name = GameData.ResName(cost[i].Type).ToLower(); parts[i] = ok ? $"{cost[i].Amount:0} {name}" : $"{cost[i].Amount:0} {name} (есть {have:0})"; } return string.Join(", ", parts); } // ---------------- Склад: полоска и время до заполнения ---------------- static void StorageBar(RectTransform card, IEconomy economy, ResourceType res) { string name = GameData.ResName(res); LiveBar(card, () => { float cap = economy.GetCapacity(res); return float.IsInfinity(cap) || cap <= 0 ? 0f : economy.Get(res) / cap; }, () => { float cap = economy.GetCapacity(res); return float.IsInfinity(cap) ? $"{name}: {economy.Get(res):0}" : $"{name}: {economy.Get(res):0} / {cap:0}"; }, new Color(0.85f, 0.65f, 0.25f)); } static string FillTimeText(IEconomy economy, ResourceType res, float ratePerHour) { float cap = economy.GetCapacity(res), have = economy.Get(res); if (float.IsInfinity(cap)) return ""; if (have >= cap) return "Склад полон — добыча пропадает. Улучшите Склад или Амбар."; if (ratePerHour <= 0) return "Добычи нет — поставьте рабочих"; float hours = (cap - have) / ratePerHour; return hours < 1f ? $"Склад заполнится через {Mathf.CeilToInt(hours * 60)} мин" : $"Склад заполнится через {hours:0.#} ч"; } // ---------------- Список рабочих по всем профессиям ---------------- static void WorkersList(RectTransform card) { if (!GameServices.TryGet(out IPopulation pop) || !GameServices.TryGet(out IProduction prod)) { UIKit.Text(card, "Модули Населения и Экономики не подключены", 26, UIKit.Theme.Red, TextAlignmentOptions.Center); return; } for (int j = 0; j < WorkJobs.Count; j++) { var job = (WorkJob)j; var row = UIKit.Row(card, 72, 8); var label = Live(row, () => { int slots = prod.Slots(job); if (slots <= 0) return $"{WorkJobs.Name(job)}: нет зданий"; return $"{WorkJobs.Name(job)}: {pop.GetWorkers(job)} / {slots} +{prod.RatePerHour(WorkJobs.Resource(job)):0.#}/ч"; }, 24, UIKit.Theme.Text); label.enableAutoSizing = true; label.fontSizeMin = 14; label.fontSizeMax = 24; label.alignment = TextAlignmentOptions.Left; UIKit.Size(label, -1, 72, 1); StepButton(row, "−5", job, -5); StepButton(row, "−1", job, -1); StepButton(row, "+1", job, +1); StepButton(row, "+5", job, +5); } Live(card, () => $"Свободных жителей: {pop.FreeWorkers} из {pop.Civilians}", 24, UIKit.Theme.Gold); } // Кнопка "−" / "+": активна, только если есть кого снять или куда и кого поставить static void StepButton(Transform row, string label, WorkJob job, int delta) { Button b = null; b = UIKit.Button(row, label, delta > 0 ? UIKit.Theme.Blue : UIKit.Theme.Gray, () => { if (GameServices.TryGet(out IPopulation pop)) pop.SetWorkers(job, pop.GetWorkers(job) + delta); Tick(); }, 86, 64, 24); live.Add(() => { if (b == null || !GameServices.TryGet(out IPopulation pop) || !GameServices.TryGet(out IProduction prod)) return; int workers = pop.GetWorkers(job), slots = prod.Slots(job); b.interactable = delta < 0 ? workers > 0 : slots > workers && pop.FreeWorkers > 0; }); } // ---------------- Мелочи ---------------- static void Header(RectTransform card, string text) { UIKit.Text(card, text, 28, UIKit.Theme.Gold, TextAlignmentOptions.Center).fontStyle = FontStyles.Bold; } // Текст, который обновляется, пока окно открыто static TextMeshProUGUI Live(Transform parent, Func text, float size, Color color) { var t = UIKit.Text(parent, text(), size, color, TextAlignmentOptions.Center); live.Add(() => { if (t != null) t.text = text(); }); return t; } static void LiveBar(RectTransform card, Func fraction, Func label, Color color) { var bar = UIKit.Bar(card, fraction(), color, 34, label()); live.Add(() => { if (bar != null) UIKit.SetBar(bar, fraction(), label()); }); } } // Обновляет открытое окно здания раз в полсекунды (добыча идёт, склад наполняется) public class BuildingWindowTicker : MonoBehaviour { float next; void Update() { if (Time.unscaledTime < next) return; next = Time.unscaledTime + 0.5f; BuildingWindow.Tick(); } }