using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; // Окно здания. Открывается тапом по зданию (подписей над зданиями больше нет -- всё здесь). // У каждого здания своё окно: // - добывающее (Лесопилка, Ферма, Каменоломня, Шахта, Рудник): рабочие этой профессии и кнопки "−" / "+", // добыча в час, сколько ресурса на складе и когда склад заполнится, что даст улучшение и его цена; // - Склад и Амбар: запас / лимит по их ресурсам, что даст улучшение и цена; // - Дом: сколько жилья даёт, население / предел, улучшение и цена; // - Ратуша: жильё, резерв для найма, что откроется на следующем уровне, цена улучшения; // - Кристалл: где добывается, запас / лимит; // - Казармы: очередь обучения, какие войска открыты и что нужно для остальных, улучшение (открывает войска), // кнопка "Нанять войска" (панель "Войска"). // - Стройка и улучшение идут по времени (BuildTimer, 01.11, ТЕСТ): полоска, сколько осталось, ускорение за кристаллы. // Пока здание строится, в окне только стройка; при улучшении здание работает на старом уровне. // - У построенных зданий (кроме Ратуши) -- кнопки "Перенести" (режим стройки) и "Снести" (часть цены возвращается). // Без здания (кнопка "Рабочие" в панели 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; var hospital = focus != null ? focus.GetComponent() : null; var heroCastle = focus != null ? focus.GetComponent() : null; bool constructing = focus != null && BuildTimer.IsConstructing(focus.transform); if (focus != null) TimerBlock(card, focus); if (focus == null) WorkersList(card); else if (constructing) UIKit.Text(card, "Пока здание строится, оно не работает.", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); else if (townHall != null) TownHallSection(card, townHall); else if (barracks != null) BarracksSection(card, barracks); else if (hospital != null) HospitalSection(card, hospital); else if (heroCastle != null) HeroCastleSection(card); 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); if (focus != null) ActionsBlock(card, focus); UIKit.Button(card, "Закрыть", UIKit.Theme.Gray, Close, -1, 80, 28); } // Стройка закончилась (BuildTimer.Completed): окно этого здания показывает новый уровень internal static void OnBuildCompleted(BuildTimer timer) { if (root != null && timer != null && focus == timer.gameObject) Rebuild(); } 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); if (GameServices.TryGet(out IResourceProtection protection)) { Header(card, "Защита от грабежа"); var p = EconomyConfig.Load().protection; ResourceType[] kinds = warehouse ? new[] { ResourceType.Wood, ResourceType.Stone, ResourceType.Iron, ResourceType.Gold } : new[] { ResourceType.Food }; Live(card, () => { var lines = new List(); foreach (var k in kinds) lines.Add($"{GameData.ResName(k)}: спрятано {protection.Protected(k):0}, можно унести {protection.Lootable(k):0}"); return string.Join("\n", lines.ToArray()); }, 22, UIKit.Theme.Text); UIKit.Text(card, warehouse ? $"Каждый уровень Склада прячет ещё {p.perWarehouseLevel:0} каждого ресурса — в набеге их не унесут." : $"Каждый уровень Амбара прячет ещё {p.perGranaryLevel:0} еды — в набеге её не унесут.", 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); string more = MoreBuildingsAt(level, next); if (more.Length > 0) UIKit.Text(card, "Больше зданий: " + more, 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (TimerRunning(townHall.gameObject)) return; // улучшение уже идёт -- полоска сверху var cost = BuildingData.ToAmounts(townHall.GetUpgradeCost()); UIKit.Text(card, "Цена: " + CostText(cost), 24, UIKit.Theme.Text, TextAlignmentOptions.Center); long ms = config.ConstructionMs(true, next); TimeText(card, ms); bool canPay = townHall.CanUpgrade(); var b = UIKit.Button(card, $"Улучшить до {next} ур.", canPay ? UIKit.Theme.Green : UIKit.Theme.Gray, () => { if (!townHall.CanUpgrade()) { UIKit.Toast("Не хватает ресурсов"); return; } if (ms > 0 && BuildTimer.FreeBuilders == 0) { UIKit.Toast("Все строители заняты"); return; } if (!GameServices.TryGet(out IEconomy e) || !e.TrySpend(cost)) { UIKit.Toast("Не хватает ресурсов"); return; } if (ms <= 0) BuildTimer.Begin(townHall.gameObject, BuildTimer.Kind.Upgrade, next, 0, 0).Complete(); else { long now = GameClock.UnixNowMs; BuildTimer.Begin(townHall.gameObject, BuildTimer.Kind.Upgrade, next, now, now + ms); UIKit.Toast($"Ратуша улучшается до {next} уровня"); } Rebuild(); }, -1, 80, 26); b.interactable = canPay; } // Каких зданий на следующем уровне Ратуши можно построить больше: "Лес 2 → 3, Дом 3 → 4" static string MoreBuildingsAt(int level, int next) { var config = EconomyConfig.Load(); var parts = new List(); if (config.buildingLimits != null) foreach (var l in config.buildingLimits) { if (l == null) continue; int a = config.BuildingLimitFor(l.buildingName, level), b = config.BuildingLimitFor(l.buildingName, next); if (b > a) parts.Add($"{l.buildingName} {a} → {b}"); } return string.Join(", ", parts.ToArray()); } // ---------------- Казармы ---------------- 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 training = gp.Training; if (training.Count == 0) return "Никто не обучается"; var o = training[0]; string more = training.Count > 1 ? $" (ещё заказов: {training.Count - 1})" : ""; return $"{GameData.Troops[o.Type].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); } // ---------------- Госпиталь (решения 31.10, п. 7) ---------------- static void HospitalSection(RectTransform card, Hospital hospital) { UIKit.Text(card, $"Госпиталь лечит раненых после боёв. Мест: {Hospital.BedsAt(Hospital.MaxLevel)} " + "(считается самый прокачанный госпиталь). Кто не поместился -- ждёт места.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (GameServices.TryGet(out IArmy army)) { Header(card, "Раненые"); Live(card, () => { int healingNow = 0, waiting = 0; long readyMs = long.MaxValue; foreach (var h in army.Healing) { healingNow += h.Count; readyMs = Math.Min(readyMs, h.EndMs); } for (int t = 0; t < army.TypeCount; t++) waiting += army.GetWoundedWaiting(t); if (healingNow + waiting == 0) return "Раненых нет"; string next = readyMs != long.MaxValue ? $", ближайшие вылечатся через {GameData.FormatTime(Math.Max(0, readyMs - GameClock.UnixNowMs) / 1000.0)}" : ""; return $"Лечатся {healingNow} из {army.HospitalBeds} мест, ждут места {waiting}{next}"; }, 24, UIKit.Theme.Text); Live(card, () => { var lines = new List(); foreach (var h in army.Healing) if (h.Type >= 0 && h.Type < GameData.Troops.Count) lines.Add($"{GameData.Troops[h.Type].name} ×{h.Count} — {GameData.FormatTime(Math.Max(0, h.EndMs - GameClock.UnixNowMs) / 1000.0)}"); return lines.Count > 0 ? string.Join("\n", lines.ToArray()) : ""; }, 22, UIKit.Theme.TextDim); var rules = HospitalConfig.Load().healGameHours; UIKit.Text(card, $"Лечение одного воина: пехота и лучники {rules[0]}, кавалерия {rules[2]}, осадные {rules[3]} игровых ч " + "(игровой час = 5 мин). Лечатся и пока игра закрыта.", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); } UpgradeBlock(card, hospital.level, $"мест {Hospital.BedsAt(hospital.level)} → {Hospital.BedsAt(hospital.level + 1)}", () => hospital.Level = hospital.level + 1, Hospital.MaxBuildLevel); } // ---------------- Терем богатыря: окно героя ---------------- // Числа героя -- те же, что в панели "Герой" (GameProgress / GameData), механика не меняется. static void HeroCastleSection(RectTransform card) { var gp = GameProgress.Instance; UIKit.Text(card, "В Тереме живёт богатырь -- верховный командир. Он гуляет по городу; тапните по нему, чтобы поговорить.", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center); if (gp == null) return; int lvl = gp.Data.heroLevel, maxLvl = gp.HeroMaxLevel; var head = UIKit.Row(card, 140, 16); UIKit.Icon(head, new Color(0.55f, 0.4f, 0.2f), "Б", 130); // место под портрет var info = UIKit.Card(head, new Color(0, 0, 0, 0), 0, 4); UIKit.Size(info, -1, -1, 1); UIKit.Text(info, $"Уровень {lvl} / {maxLvl}", 30, UIKit.Theme.Gold).fontStyle = FontStyles.Bold; UIKit.Text(info, $"Сила героя: {GameData.HeroPower(lvl)}", 24, UIKit.Theme.Text); UIKit.Text(info, $"Бонус к силе армии: +{Mathf.RoundToInt(gp.ArmyBonus * 100)}%", 24, UIKit.Theme.Text); Header(card, "Навыки"); for (int i = 0; i < GameData.HeroSkillNames.Length; i++) { bool has = gp.HasHeroSkill(i); UIKit.Text(card, has ? $"✓ {GameData.HeroSkillNames[i]}" : $"{GameData.HeroSkillNames[i]} — с {GameData.HeroSkillLevels[i]} ур.", 22, has ? UIKit.Theme.Text : UIKit.Theme.TextDim); } Header(card, "Улучшение героя"); if (lvl >= maxLvl) { UIKit.Text(card, $"Предел уровня. Улучшите Ратушу до {gp.TownHallLevel + 1} ур., чтобы поднять предел до " + $"{GameData.HeroMaxLevel(gp.TownHallLevel + 1)}.", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); } else { var cost = GameData.HeroUpgradeCost(lvl); Live(card, () => { var parts = new List(); foreach (var c in cost) { bool enough = gp.Res(c.type) >= c.amount; parts.Add($"{GameData.ResName(c.type)} {c.amount:N0}"); } return "Цена: " + string.Join(", ", parts.ToArray()); }, 22, UIKit.Theme.Text); UIKit.Text(card, $"Дальше: сила {GameData.HeroPower(lvl)} → {GameData.HeroPower(lvl + 1)}, " + $"бонус армии +{GameData.HeroArmyBonusPercent(lvl)}% → +{GameData.HeroArmyBonusPercent(lvl + 1)}%", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center); var up = UIKit.Button(card, $"Улучшить до {lvl + 1} ур.", UIKit.Theme.Green, () => { if (!gp.CanUpgradeHero) { UIKit.Toast("Не хватает ресурсов"); return; } gp.UpgradeHero(); Rebuild(); }, -1, 80, 26); live.Add(() => up.interactable = gp.CanUpgradeHero); } UIKit.Button(card, "Подробнее о герое", UIKit.Theme.Blue, () => { Close(); if (UIPanelManager.Instance != null) UIPanelManager.Instance.Open("Hero"); }, -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, int maxLevel = 0) { var config = EconomyConfig.Load(); if (level >= (maxLevel > 0 ? maxLevel : config.upgrade.maxLevel)) // у госпиталя свой предел: I-V { UIKit.Text(card, "Максимальный уровень", 26, UIKit.Theme.Gold, TextAlignmentOptions.Center); return; } Header(card, $"Улучшение до {level + 1} ур."); UIKit.Text(card, effect, 24, UIKit.Theme.Text, TextAlignmentOptions.Center); if (focus != null && TimerRunning(focus)) return; // улучшение уже идёт -- полоска сверху var cost = config.UpgradeCost(level); UIKit.Text(card, "Цена: " + CostText(cost), 24, UIKit.Theme.Text, TextAlignmentOptions.Center); long ms = config.ConstructionMs(false, level + 1); TimeText(card, ms); var building = focus; bool canPay = GameServices.TryGet(out IEconomy economy) && economy.CanAfford(cost); var b = UIKit.Button(card, $"Улучшить до {level + 1} ур.", canPay ? UIKit.Theme.Green : UIKit.Theme.Gray, () => { bool timed = ms > 0 && building != null; if (timed && BuildTimer.FreeBuilders == 0) { UIKit.Toast("Все строители заняты"); return; } if (!GameServices.TryGet(out IEconomy e) || !e.TrySpend(cost)) { UIKit.Toast("Не хватает ресурсов"); return; } if (timed) { long now = GameClock.UnixNowMs; BuildTimer.Begin(building, BuildTimer.Kind.Upgrade, level + 1, now, now + ms); UIKit.Toast($"Улучшение до {level + 1} уровня началось"); } else { raiseLevel(); if (CitySync.Instance != null) CitySync.Instance.SyncNow(); UIKit.Toast($"Улучшено до {level + 1} уровня"); } Rebuild(); }, -1, 80, 26); b.interactable = canPay; } // ---------------- Стройка по времени (BuildTimer) ---------------- static bool TimerRunning(GameObject building) { var t = BuildTimer.Of(building); return t != null && BuildTimer.All.Contains(t); } static void TimeText(RectTransform card, long ms) { if (ms <= 0) return; Live(card, () => $"Время: {GameData.FormatTime(ms / 1000.0)} • строителей свободно: {BuildTimer.FreeBuilders} из {BuildTimer.Builders}", 22, UIKit.Theme.TextDim); } // Полоска стройки или улучшения, сколько осталось, ускорение за кристаллы static void TimerBlock(RectTransform card, GameObject building) { var t = BuildTimer.Of(building); if (t == null || !BuildTimer.All.Contains(t)) return; Header(card, t.IsConstruction ? "Идёт стройка" : $"Идёт улучшение до {t.targetLevel} ур."); var bar = UIKit.Bar(card, t.Progress, UIKit.Theme.Gold, 26); live.Add(() => { if (t != null) UIKit.SetBar(bar, t.Progress); }); Live(card, () => t != null ? "Осталось " + GameData.FormatTime(t.SecondsLeft) : "Готово", 24, UIKit.Theme.Text); int perGem = Mathf.Max(1, EconomyConfig.Load().construction.secondsPerGem); Func gems = () => t != null ? Mathf.Max(1, Mathf.CeilToInt((float)t.SecondsLeft / perGem)) : 0; var speed = UIKit.Button(card, "", UIKit.Theme.Blue, () => { if (t == null) return; int price = gems(); if (!GameServices.TryGet(out IEconomy e) || !e.TrySpend(ResourceType.Gems, price)) { UIKit.Toast($"Нужно {price} кристаллов"); return; } t.Complete(); // окно перестроит OnBuildCompleted }, -1, 80, 26); live.Add(() => { if (t != null) UIKit.SetButton(speed, $"Ускорить за {gems()} кристаллов", true); }); UIKit.SetButton(speed, $"Ускорить за {gems()} кристаллов", true); } // ---------------- Перенос и снос ---------------- static void ActionsBlock(RectTransform card, GameObject building) { var id = building.GetComponent(); if (id == null || string.IsNullOrEmpty(id.buildingName)) return; // Ратуша и стартовые постройки сцены var row = UIKit.Row(card, 80, 12); var move = UIKit.Button(row, "Перенести", UIKit.Theme.Blue, () => { Close(); if (BuildModeController.Instance != null) BuildModeController.Instance.StartMove(building); }, -1, 80, 26); UIKit.Size(move, -1, 80, 1); float share = EconomyConfig.Load().construction.demolishRefund; var demolish = UIKit.Button(row, "Снести", UIKit.Theme.Red, () => { UIKit.Confirm("Снести здание?", $"«{id.buildingName}» будет снесено. Вернётся {Mathf.RoundToInt(share * 100)}% цены постройки: {RefundText(id.buildingName, share)}.", "Снести", () => Demolish(building)); }, -1, 80, 26); UIKit.Size(demolish, -1, 80, 1); } static ResourceAmount[] Refund(string buildingName, float share) { var data = BuildManager.Instance != null ? BuildManager.Instance.FindData(buildingName) : null; if (data == null || data.cost == null) return new ResourceAmount[0]; var result = new ResourceAmount[data.cost.Length]; for (int i = 0; i < result.Length; i++) result[i] = new ResourceAmount(data.cost[i].type, Mathf.Floor(data.cost[i].amount * share)); return result; } static string RefundText(string buildingName, float share) { var parts = new List(); foreach (var a in Refund(buildingName, share)) if (a.Amount > 0) parts.Add($"{a.Amount:0} {GameData.ResName(a.Type)}"); return parts.Count > 0 ? string.Join(", ", parts.ToArray()) : "ничего"; } static void Demolish(GameObject building) { if (building == null) return; var id = building.GetComponent(); if (id == null || string.IsNullOrEmpty(id.buildingName)) return; string name = id.buildingName; if (GameServices.TryGet(out IEconomy economy)) foreach (var a in Refund(name, EconomyConfig.Load().construction.demolishRefund)) if (a.Amount > 0) economy.Add(a.Type, a.Amount); var timer = building.GetComponent(); if (timer != null) timer.Discard(); // строитель свободен сразу id.buildingName = null; // сохранение и пересчёт города в этом кадре его уже не видят UnityEngine.Object.Destroy(building); Close(); if (CitySync.Instance != null) CitySync.Instance.SyncNow(); if (BuildGrid.Instance != null) BuildGrid.Instance.Refresh(); UIKit.Toast($"Снесено: {name}"); } // Цена: чего хватает -- зелёным, чего нет -- красным, со сколько есть 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(); } }