2026-10-31 20:52:28 +03:00
using System ;
using System.Collections.Generic ;
2026-10-27 18:51:35 +03:00
using TMPro ;
using UnityEngine ;
2026-10-31 20:52:28 +03:00
using UnityEngine.UI ;
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
// Окно здания. Открывается тапом по зданию (подписей над зданиями больше нет -- всё здесь).
// У каждого здания своё окно:
// - добывающее (Лесопилка, Ферма, Каменоломня, Шахта, Рудник): рабочие этой профессии и кнопки "− " / "+",
// добыча в час, сколько ресурса на складе и когда склад заполнится, что даст улучшение и его цена;
// - Склад и Амбар: запас / лимит по их ресурсам, что даст улучшение и цена;
// - Дом: сколько жилья даёт, население / предел, улучшение и цена;
// - Ратуша: жильё, резерв для найма, что откроется на следующем уровне, цена улучшения;
// - Кристалл: где добывается, запас / лимит;
// - Казармы: очередь обучения, какие войска открыты и что нужно для остальных, улучшение (открывает войска),
// кнопка "Нанять войска" (панель "Войска").
// Без здания (кнопка "Рабочие" в панели DEV и "Все рабочие" в окне) -- список рабочих по всем профессиям.
// Окно -- только интерфейс: рабочих ставит модуль Населения, добычу считает модуль Экономики.
// Пока окно открыто, цифры обновляются сами (раз в полсекунды).
2026-10-27 18:51:35 +03:00
public static class BuildingWindow
{
static GameObject root ;
2026-10-31 20:52:28 +03:00
static GameObject focus ; // здание, по которому тапнули (null -- список рабочих)
static readonly List < Action > live = new List < Action >(); // что обновлять, пока окно открыто
2026-10-27 18:51:35 +03:00
public static void OnWorkplaceTapped ( ResourceCollector building ) { Open ( building != null ? building . gameObject : null ); }
public static void OnStorageTapped ( StorageBuilding building ) { Open ( building != null ? building . gameObject : null ); }
2026-10-31 20:52:28 +03:00
public static void OnTownHallTapped ( TownHall townHall ) { Open ( townHall != null ? townHall . gameObject : null ); }
2026-10-27 18:51:35 +03:00
public static void Open ( GameObject building )
{
2026-10-31 20:52:28 +03:00
// В режиме стройки тап -- это выбор места, а не здания
2026-10-27 18:51:35 +03:00
if ( BuildModeController . Instance != null && BuildModeController . Instance . IsActive ) return ;
focus = building ;
Rebuild ();
}
public static void Close ()
{
2026-10-31 20:52:28 +03:00
if ( root != null ) UnityEngine . Object . Destroy ( root );
2026-10-27 18:51:35 +03:00
root = null ;
focus = null ;
2026-10-31 20:52:28 +03:00
live . Clear ();
2026-10-27 18:51:35 +03:00
}
static void Rebuild ()
{
2026-10-31 20:52:28 +03:00
if ( root != null ) UnityEngine . Object . Destroy ( root );
live . Clear ();
2026-10-27 18:51:35 +03:00
var card = UIKit . Modal ( TitleFor ( focus ), out root );
2026-10-31 20:52:28 +03:00
root . AddComponent < BuildingWindowTicker >();
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
var rc = focus != null ? focus . GetComponent < ResourceCollector >() : null ;
var storage = focus != null ? focus . GetComponent < StorageBuilding >() : null ;
var townHall = focus != null ? focus . GetComponent < TownHall >() : null ;
var barracks = focus != null ? focus . GetComponent < Barracks >() : null ;
2026-11-01 13:52:47 +01:00
var hospital = focus != null ? focus . GetComponent < Hospital >() : null ;
2026-10-31 20:52:28 +03:00
if ( focus == null ) WorkersList ( card );
else if ( townHall != null ) TownHallSection ( card , townHall );
else if ( barracks != null ) BarracksSection ( card , barracks );
2026-11-01 13:52:47 +01:00
else if ( hospital != null ) HospitalSection ( card , hospital );
2026-10-31 20:52:28 +03:00
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 );
2026-10-27 18:51:35 +03:00
UIKit . Button ( card , "Закрыть" , UIKit . Theme . Gray , Close , - 1 , 80 , 28 );
}
2026-10-31 20:52:28 +03:00
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 ;
2026-10-27 18:51:35 +03:00
static string TitleFor ( GameObject building )
{
2026-10-31 20:52:28 +03:00
focusSet = building != null ;
2026-10-27 18:51:35 +03:00
if ( building == null ) return "РАБОЧИЕ" ;
2026-10-31 20:52:28 +03:00
var townHall = building . GetComponent < TownHall >();
if ( townHall != null ) return $"РАТУША — {townHall.currentLevel} УР." ;
2026-10-27 18:51:35 +03:00
var id = building . GetComponent < PlacedBuildingId >();
var leveled = building . GetComponent < ILeveledBuilding >();
string name = id != null && ! string . IsNullOrEmpty ( id . buildingName ) ? id . buildingName : building . name ;
return leveled != null ? $"{name.ToUpper()} — {leveled.Level} УР." : name . ToUpper ();
}
2026-10-31 20:52:28 +03:00
// ---------------- Добывающее здание ----------------
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
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 ? " <color=#ff7a7a>голод: добыча × 0.5</color>" : "" ;
return $"Сейчас: <color=#9be29b>+{rate:0.#} {resName} в час</color>{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 )
2026-10-27 18:51:35 +03:00
{
var config = EconomyConfig . Load ();
2026-10-31 20:52:28 +03:00
int level = townHall . currentLevel ;
int next = level + 1 ;
var p = config . population ;
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
UIKit . Text ( card , $"Ратуша даёт {p.housingBase + p.housingPerTownHallLevel * (level - 1)} мест жилья. От её уровня зависят резерв для найма и доступные здания и войска." ,
24 , UIKit . Theme . TextDim , TextAlignmentOptions . Center );
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
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 );
}
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
if ( level >= townHall . maxLevel )
2026-10-27 18:51:35 +03:00
{
UIKit . Text ( card , "Максимальный уровень" , 26 , UIKit . Theme . Gold , TextAlignmentOptions . Center );
return ;
}
2026-10-31 20:52:28 +03:00
// Что даст следующий уровень
Header ( card , $"Уровень {next}" );
var gains = new List < string > { $"жильё +{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 , () =>
2026-10-27 18:51:35 +03:00
{
2026-10-31 20:52:28 +03:00
if (! townHall . CanUpgrade ()) { UIKit . Toast ( "Не хватает ресурсов" ); return ; }
townHall . Upgrade ();
2026-10-27 18:51:35 +03:00
if ( CitySync . Instance != null ) CitySync . Instance . SyncNow ();
2026-10-31 20:52:28 +03:00
UIKit . Toast ( $"Ратуша улучшена до {townHall.currentLevel} уровня" );
2026-10-27 18:51:35 +03:00
Rebuild ();
}, - 1 , 80 , 26 );
b . interactable = canPay ;
}
2026-10-31 20:52:28 +03:00
// ---------------- Казармы ----------------
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 , () =>
{
2026-11-01 13:19:34 +01:00
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}" ;
2026-10-31 20:52:28 +03:00
}, 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
? $"<color=#9be59b>{GameData.Troops[i].name} — открыты</color>"
: $"{GameData.Troops[i].name} — нужно: {lockText}" ,
24 , lockText . Length == 0 ? UIKit . Theme . Text : UIKit . Theme . Gold , TextAlignmentOptions . Center );
}
}
// Что откроет следующий уровень этих Казарм
var opens = new List < string >();
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 );
}
2026-11-01 13:52:47 +01:00
// ---------------- Госпиталь (решения 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 < string >();
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 );
}
2026-10-31 20:52:28 +03:00
// Резерв для найма на уровне Ратуши (те же числа, что у модуля Населения)
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 < string >();
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 ());
}
// ---------------- Улучшение (общее для зданий с уровнем, кроме Ратуши) ----------------
2026-11-01 13:52:47 +01:00
static void UpgradeBlock ( RectTransform card , int level , string effect , Action raiseLevel , int maxLevel = 0 )
2026-10-31 20:52:28 +03:00
{
var config = EconomyConfig . Load ();
2026-11-01 13:52:47 +01:00
if ( level >= ( maxLevel > 0 ? maxLevel : config . upgrade . maxLevel )) // у госпиталя свой предел: I-V
2026-10-31 20:52:28 +03:00
{
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 ;
}
// Цена: чего хватает -- зелёным, чего нет -- красным, со сколько есть
2026-10-27 18:51:35 +03:00
static string CostText ( ResourceAmount [] cost )
{
2026-10-31 20:52:28 +03:00
GameServices . TryGet ( out IEconomy economy );
2026-10-27 18:51:35 +03:00
var parts = new string [ cost . Length ];
2026-10-31 20:52:28 +03:00
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
? $"<color=#9be59b>{cost[i].Amount:0}</color> {name}"
: $"<color=#ff7a7a>{cost[i].Amount:0}</color> {name} (есть {have:0})" ;
}
2026-10-27 18:51:35 +03:00
return string . Join ( ", " , parts );
}
2026-10-31 20:52:28 +03:00
// ---------------- Склад: полоска и время до заполнения ----------------
2026-10-27 18:51:35 +03:00
2026-10-31 20:52:28 +03:00
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 "<color=#ff7a7a>Склад полон — добыча пропадает. Улучшите Склад или Амбар.</color>" ;
if ( ratePerHour <= 0 ) return "Добычи нет — поставьте рабочих" ;
float hours = ( cap - have ) / ratePerHour ;
return hours < 1f ? $"Склад заполнится через {Mathf.CeilToInt(hours * 60)} мин" : $"Склад заполнится через {hours:0.#} ч" ;
}
// ---------------- Список рабочих по всем профессиям ----------------
static void WorkersList ( RectTransform card )
2026-10-27 18:51:35 +03:00
{
if (! GameServices . TryGet ( out IPopulation pop ) || ! GameServices . TryGet ( out IProduction prod ))
{
2026-10-31 20:52:28 +03:00
UIKit . Text ( card , "Модули Населения и Экономики не подключены" , 26 , UIKit . Theme . Red , TextAlignmentOptions . Center );
2026-10-27 18:51:35 +03:00
return ;
}
for ( int j = 0 ; j < WorkJobs . Count ; j ++)
{
var job = ( WorkJob ) j ;
var row = UIKit . Row ( card , 72 , 8 );
2026-10-31 20:52:28 +03:00
var label = Live ( row , () =>
{
int slots = prod . Slots ( job );
if ( slots <= 0 ) return $"{WorkJobs.Name(job)}: нет зданий" ;
return $"{WorkJobs.Name(job)}: {pop.GetWorkers(job)} / {slots} <color=#9be29b>+{prod.RatePerHour(WorkJobs.Resource(job)):0.#}/ч</color>" ;
}, 24 , UIKit . Theme . Text );
2026-10-27 18:51:35 +03:00
label . enableAutoSizing = true ; label . fontSizeMin = 14 ; label . fontSizeMax = 24 ;
2026-10-31 20:52:28 +03:00
label . alignment = TextAlignmentOptions . Left ;
2026-10-27 18:51:35 +03:00
UIKit . Size ( label , - 1 , 72 , 1 );
2026-10-31 20:52:28 +03:00
StepButton ( row , "− 5" , job , - 5 );
StepButton ( row , "− 1" , job , - 1 );
StepButton ( row , "+1" , job , + 1 );
StepButton ( row , "+5" , job , + 5 );
2026-10-27 18:51:35 +03:00
}
2026-10-31 20:52:28 +03:00
Live ( card , () => $"Свободных жителей: <b>{pop.FreeWorkers}</b> из {pop.Civilians}" , 24 , UIKit . Theme . Gold );
2026-10-27 18:51:35 +03:00
}
2026-10-31 20:52:28 +03:00
// Кнопка "− " / "+": активна, только если есть кого снять или куда и кого поставить
static void StepButton ( Transform row , string label , WorkJob job , int delta )
2026-10-27 18:51:35 +03:00
{
2026-10-31 20:52:28 +03:00
Button b = null ;
b = UIKit . Button ( row , label , delta > 0 ? UIKit . Theme . Blue : UIKit . Theme . Gray , () =>
2026-10-27 18:51:35 +03:00
{
if ( GameServices . TryGet ( out IPopulation pop )) pop . SetWorkers ( job , pop . GetWorkers ( job ) + delta );
2026-10-31 20:52:28 +03:00
Tick ();
2026-10-27 18:51:35 +03:00
}, 86 , 64 , 24 );
2026-10-31 20:52:28 +03:00
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 < string > 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 < float > fraction , Func < string > 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 ();
2026-10-27 18:51:35 +03:00
}
}