2026-11-06 16:13:43 +01:00
using System.Collections.Generic ;
2026-10-27 17:59:16 +03:00
using System ;
using UnityEngine ;
// ТАБЛИЦА БАЛАНСА ЭКОНОМИКИ. Все числа экономики -- здесь, а не в коде модулей.
//
// Ассет: Assets/Resources/EconomyConfig.asset (выдели его в окне Project и меняй числа в Inspector,
// перекомпиляция не нужна). Создать заново: правый клик в Project -> Create -> Bogatyri -> Economy Config,
// имя файла -- EconomyConfig, папка -- Resources.
//
// Модули конфиг не видят: App/GameModules переводит его в правила модулей (например, правила Населения).
// Числа -- из дизайн-документа. Помеченные "не утверждено" -- временные, их решает команда.
[CreateAssetMenu(fileName = "EconomyConfig", menuName = "Bogatyri/Economy Config")]
public class EconomyConfig : ScriptableObject
{
[Serializable]
public class PopulationSettings
{
[Tooltip("Стартовый город T1: всего людей")]
public int startTotal = 300 ;
[Tooltip("Из них рабочих")]
public int startWorkers = 120 ;
[Tooltip("Из них воинов (выдаются в Армию пехотой при новой игре)")]
public int startSoldiers = 30 ;
[Tooltip("Еды в час на одного жителя")]
public float foodPerCivilianPerHour = 0.1f ;
2026-11-04 15:13:02 +01:00
[Tooltip("Еды в час на одного воина (и новобранца в Казармах), если норма по типам не задана")]
2026-10-27 17:59:16 +03:00
public float foodPerSoldierPerHour = 0.2f ;
2026-11-04 15:13:02 +01:00
[Tooltip("Еды в час на воина по типам: пехота, лучники, кавалерия, осадные. Решения 01.11, п. 4: 0,15 / 0,20 / 0,15 / 0,15. " +
"Раненые едят так же, даже ожидая койку. Пусто -- все по foodPerSoldierPerHour")]
public float [] foodPerTroopTypePerHour = { 0.15f , 0.2f , 0.15f , 0.15f };
2026-10-27 17:59:16 +03:00
[Tooltip("Прирост людей в час, если есть еда (документ: 2–3)")]
public float growthPerHour = 2.5f ;
2026-10-27 18:51:35 +03:00
[Tooltip("Предел населения без Домов на 1 уровне Ратуши (решение команды от 19.10: 300 при 300 -- прирост стоит)")]
public int housingBase = 300 ;
2026-10-27 17:59:16 +03:00
[Tooltip("Прибавка к пределу за каждый следующий уровень Ратуши. Не утверждено")]
2026-10-27 18:51:35 +03:00
public int housingPerTownHallLevel = 50 ;
[Tooltip("Прибавка к пределу за каждый уровень Дома (здание \"Население\"). Не утверждено")]
public int housingPerHouseLevel = 50 ;
2026-10-27 17:59:16 +03:00
[Tooltip("Офлайн-прогресс считается не больше чем за столько часов. Не утверждено")]
public int maxOfflineHours = 48 ;
2026-10-28 22:11:36 +03:00
2026-10-31 19:34:33 +03:00
[Tooltip("Гражданский резерв по уровню Ратуши: [0] -- уровень 1 , [ 1 ] -- уровень 2. .. Столько свободных жителей нельзя забрать в войско . Регламент 31.10 , ТЕСТ : 1 - 2 -- 10 , 3 - 5 -- 20 , 6 - 10 -- 30 ")]
public int [] civilianReserveByTownHallLevel = { 10 , 10 , 20 , 20 , 20 , 30 , 30 , 30 , 30 , 30 };
2026-10-27 17:59:16 +03:00
}
[Serializable]
2026-10-27 18:51:35 +03:00
public class ProductionSettings // этап 3: производство через рабочих
2026-10-27 17:59:16 +03:00
{
[Tooltip("Лесоруб T1: дерева в час")]
public float woodcutterPerHour = 2f ;
[Tooltip("Фермер T1: еды в час")]
public float farmerPerHour = 2f ;
[Tooltip("Каменотёс T1: камня в час")]
public float stonecutterPerHour = 1.5f ;
[Tooltip("Шахтёр T1: металла в час")]
public float minerIronPerHour = 1f ;
[Tooltip("Шахтёр T1: золота в час")]
public float minerGoldPerHour = 0.4f ;
[Tooltip("Мест для рабочих в здании T1 (Лесопилка и т.п.)")]
public int workerSlotsT1 = 30 ;
2026-10-27 18:51:35 +03:00
[Tooltip("+ мест за каждый уровень здания выше первого. Не утверждено")]
public int workerSlotsPerLevel = 10 ;
[Tooltip("+ к норме за каждый уровень здания выше первого (0.1 = +10%). Не утверждено")]
public float efficiencyPerLevel = 0.1f ;
2026-10-31 19:34:33 +03:00
[Tooltip("Голод: множитель добычи всех ресурсов, кроме еды (0.5 -- вдвое меньше). Еда добывается без штрафа. Регламент 31.10, ТЕСТ")]
[Range(0f, 1f)] public float famineNonFoodMultiplier = 0.5f ;
2026-10-27 18:51:35 +03:00
}
2026-11-06 16:13:43 +01:00
[Serializable]
public class CrystalExpansion
{
public int townHallLevel = 3 ;
public int capacity = 75 ;
public float gold , wood , stone , iron ;
public CrystalExpansion () { }
public CrystalExpansion ( int townHall , int cap , float gold , float wood , float stone , float iron )
{
townHallLevel = townHall ; capacity = cap ; this . gold = gold ; this . wood = wood ; this . stone = stone ; this . iron = iron ;
}
}
2026-10-27 18:51:35 +03:00
[Serializable]
public class StorageSettings // этап 1: лимиты хранения
{
[Tooltip("Лимит без Складов: дерево, камень, металл, золото. Не утверждено")]
public float baseGoods = 1000f ;
[Tooltip("Лимит еды без Амбаров. Не утверждено")]
public float baseFood = 1500f ;
[Tooltip("+ к лимиту дерева, камня, металла, золота за каждый уровень Склада. Не утверждено")]
public float perWarehouseLevel = 1000f ;
[Tooltip("+ к лимиту еды за каждый уровень Амбара. Не утверждено")]
public float perGranaryLevel = 1500f ;
2026-11-06 16:13:43 +01:00
[Tooltip("Стартовая вместимость синих кристаллов (регламент валют 05.11, разд. 8). ТЕСТ")]
2026-10-27 18:51:35 +03:00
public float crystalCap = 50f ;
2026-11-06 16:13:43 +01:00
[Tooltip("Платные расширения хранилища кристаллов по порядку: с какой Ратуши, до скольки, цена (золото и обычные ресурсы). " +
"Ратуша только разрешает -- само не растёт (регламент валют 05.11, разд. 8; ответ 06.11, п. 1). Числа и цены ТЕСТ")]
public CrystalExpansion [] crystalExpansions =
{
new CrystalExpansion ( 3 , 75 , 2000 , 1500 , 1500 , 0 ),
new CrystalExpansion ( 5 , 100 , 4000 , 3000 , 3000 , 1000 ),
new CrystalExpansion ( 7 , 150 , 7000 , 5000 , 5000 , 2000 ),
new CrystalExpansion ( 9 , 200 , 10000 , 8000 , 8000 , 4000 ),
};
2026-10-27 18:51:35 +03:00
[Tooltip("Цена Склада: дерево / камень. Не утверждено")]
public float warehouseWood = 200f ;
public float warehouseStone = 100f ;
[Tooltip("Цена Амбара: дерево / камень. Не утверждено")]
public float granaryWood = 150f ;
public float granaryStone = 50f ;
}
[Serializable]
public class UpgradeSettings // улучшение зданий (рабочие места, Дома, Склады, Амбары)
{
[Tooltip("Цена улучшения с 1 на 2 уровень: дерево / камень. Не утверждено")]
public float baseWood = 150f ;
public float baseStone = 100f ;
[Tooltip("Во сколько раз дорожает каждый следующий уровень. Не утверждено")]
public float multiplierPerLevel = 1.6f ;
public int maxLevel = 10 ;
2026-10-27 17:59:16 +03:00
}
[Serializable]
public class DepositSettings // этап 4: качество месторождения (пока не используется)
{
public float poor = 0.6f ;
public float normal = 1f ;
public float rich = 1.5f ;
}
[Serializable]
2026-10-28 22:11:36 +03:00
public class SoldierSettings // этап 5: цена солдата
2026-10-27 17:59:16 +03:00
{
2026-10-28 22:11:36 +03:00
[Tooltip("Сколько жителей стоит один воин (берутся из жителей при начале обучения)")]
2026-10-27 17:59:16 +03:00
public int people = 1 ;
public float food = 15f ;
public float iron = 5f ;
public float gold = 5f ;
[Tooltip("Время подготовки, секунд. Не утверждено")]
public int trainSeconds = 60 ;
2026-10-31 20:52:28 +03:00
[Tooltip("Уровень Казарм, с которого открывается тип войск (порядок как в GameData.Troops: пехота, лучники, кавалерия, осадные). " +
"Нужен и уровень Ратуши. Считаются самые прокачанные Казармы. ТЕСТ, не утверждено")]
public int [] requiredBarracksLevel = { 1 , 2 , 3 , 5 };
2026-10-27 17:59:16 +03:00
}
2026-11-02 00:01:51 +03:00
[Serializable]
public class ConstructionSettings // время стройки и улучшения, строители (01.11, ТЕСТ)
{
[Tooltip("Сколько строек и улучшений идёт одновременно (строители). ТЕСТ")]
public int builders = 2 ;
[Tooltip("Постройка нового здания, секунд. ТЕСТ")]
public int buildSeconds = 60 ;
[Tooltip("Улучшение до уровня 2, 3, 4... (секунд): [0] -- до 2 ур ., [ 1 ] -- до 3 ур . ... ТЕСТ ")]
public int [] upgradeSecondsToLevel = { 60 , 120 , 300 , 600 , 1200 , 1800 , 3600 , 5400 , 7200 };
[Tooltip("Ратуша строится дольше остальных: множитель времени улучшения. ТЕСТ")]
public float townHallTimeMultiplier = 2f ;
2026-11-06 16:18:28 +01:00
[Tooltip("Устарело (06.11): ускорение теперь за золото -- secondsPerGold")]
2026-11-02 00:01:51 +03:00
public int secondsPerGem = 30 ;
2026-11-06 16:18:28 +01:00
[Tooltip("Ускорение стройки, улучшения и обучения войск -- за золото (06.11; регламент валют: золото -- основной ресурс " +
"построек и улучшений, синие кристаллы на ускорения не тратятся): секунд за 1 золото. ТЕСТ")]
public int secondsPerGold = 10 ;
2026-11-04 23:36:14 +03:00
// Сноса зданий нет (решение 04.11): только бесплатный перенос
2026-11-02 00:01:51 +03:00
}
[Serializable]
public class BuildingLimit
{
public string buildingName ;
[Tooltip("Сколько таких зданий можно иметь на Ратуше 1, 2, 3 ... уровня. 0 -- нельзя")]
public int [] maxByTownHall ;
public BuildingLimit () { }
public BuildingLimit ( string name , params int [] max ) { buildingName = name ; maxByTownHall = max ; }
}
[Serializable]
public class ProtectionSettings // защита ресурсов от грабежа (01.11, ТЕСТ)
{
[Tooltip("Защищено без Складов: дерево, камень, металл, золото. ТЕСТ")]
public float baseGoods = 100f ;
[Tooltip("+ к защите дерева, камня, металла, золота за каждый уровень Склада. ТЕСТ")]
public float perWarehouseLevel = 200f ;
[Tooltip("Защищено без Амбаров: еда. ТЕСТ")]
public float baseFood = 100f ;
[Tooltip("+ к защите еды за каждый уровень Амбара. ТЕСТ")]
public float perGranaryLevel = 300f ;
2026-11-04 23:36:14 +03:00
[Tooltip("Кристаллы: без технологии «Грабёж» нападающий не уносит ничего (решение 04.11). С ней -- не больше этой доли запаса. ТЕСТ, точные ограничения определят отдельно")]
[Range(0f, 1f)] public float plunderGemsShare = 0.1f ;
2026-11-02 00:01:51 +03:00
}
2026-10-27 17:59:16 +03:00
[Serializable]
public class StartResources
{
[Tooltip("Ресурсы новой игры. Не утверждено")]
public float wood = 500f ;
public float food = 500f ;
public float stone = 200f ;
public float iron = 100f ;
public float gold = 100f ;
public float gems = 0f ;
}
[Header("Население (этап 2)")]
public PopulationSettings population = new PopulationSettings ();
[Header("Стартовые ресурсы новой игры")]
public StartResources startResources = new StartResources ();
2026-10-27 18:51:35 +03:00
[Header("Склады и лимиты (этап 1)")]
public StorageSettings storage = new StorageSettings ();
2026-10-27 17:59:16 +03:00
[Header("Производство T1 (этап 3)")]
public ProductionSettings production = new ProductionSettings ();
2026-10-27 18:51:35 +03:00
[Header("Улучшение зданий")]
public UpgradeSettings upgrade = new UpgradeSettings ();
2026-10-27 17:59:16 +03:00
[Header("Месторождения (этап 4)")]
public DepositSettings deposits = new DepositSettings ();
[Header("Солдат T1 (этап 5)")]
public SoldierSettings soldier = new SoldierSettings ();
2026-11-02 00:01:51 +03:00
[Header("Стройка: время, строители, снос (ТЕСТ)")]
public ConstructionSettings construction = new ConstructionSettings ();
[Header("Лимит зданий по уровню Ратуши (ТЕСТ). Здания не из списка -- без лимита")]
public BuildingLimit [] buildingLimits =
{
// Ратуша: 1 2 3 4 5 6 7 8 9 10
new BuildingLimit ( "Лес" , 2 , 3 , 3 , 4 , 4 , 5 , 5 , 6 , 6 , 7 ),
new BuildingLimit ( "Еда" , 2 , 3 , 3 , 4 , 4 , 5 , 5 , 6 , 6 , 7 ),
new BuildingLimit ( "Камень" , 2 , 2 , 3 , 3 , 4 , 4 , 5 , 5 , 6 , 6 ),
new BuildingLimit ( "Металл" , 0 , 0 , 1 , 1 , 2 , 2 , 3 , 3 , 4 , 4 ),
new BuildingLimit ( "Золото" , 0 , 1 , 1 , 2 , 2 , 3 , 3 , 3 , 4 , 4 ),
new BuildingLimit ( "Население" , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ),
new BuildingLimit ( "Склад" , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 ),
new BuildingLimit ( "Амбар" , 1 , 1 , 2 , 2 , 2 , 3 , 3 , 3 , 4 , 4 ),
new BuildingLimit ( "Казармы" , 1 , 1 , 2 , 2 , 3 , 3 , 3 , 3 , 3 , 3 ),
new BuildingLimit ( "Госпиталь" , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
new BuildingLimit ( "Терем богатыря" , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ),
new BuildingLimit ( "Кристалл" , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 ),
};
[Header("Защита ресурсов от грабежа (ТЕСТ)")]
public ProtectionSettings protection = new ProtectionSettings ();
2026-10-27 17:59:16 +03:00
// ---------------- Загрузка ----------------
static EconomyConfig cached ;
// Конфиг из Assets/Resources/EconomyConfig.asset. Нет ассета -- числа по умолчанию (из этого файла) и предупреждение.
public static EconomyConfig Load ()
{
if ( cached != null ) return cached ;
cached = Resources . Load < EconomyConfig >( "EconomyConfig" );
if ( cached == null )
{
Debug . LogWarning ( "[ЭКОНОМИКА] Нет Assets/Resources/EconomyConfig.asset -- берутся числа по умолчанию из EconomyConfig.cs." );
cached = CreateInstance < EconomyConfig >();
}
return cached ;
}
2026-10-27 18:51:35 +03:00
// Лимит склада для ресурса по уровням Складов и Амбаров. PositiveInfinity -- без лимита (люди, сила)
public float GetCapacity ( ResourceType type , int warehouseLevels , int granaryLevels )
{
var s = storage ;
switch ( type )
{
case ResourceType . Wood :
case ResourceType . Stone :
case ResourceType . Iron :
case ResourceType . Gold : return s . baseGoods + s . perWarehouseLevel * warehouseLevels ;
case ResourceType . Food : return s . baseFood + s . perGranaryLevel * granaryLevels ;
2026-11-06 16:13:43 +01:00
case ResourceType . Gems : return CrystalStorageNow (). Capacity ; // стартовая + купленные расширения
2026-10-27 18:51:35 +03:00
default : return float . PositiveInfinity ;
}
}
2026-11-02 00:01:51 +03:00
// Сколько зданий с таким именем можно иметь на этом уровне Ратуши. -1 -- без лимита (здания нет в списке)
public int BuildingLimitFor ( string buildingName , int townHallLevel )
{
if ( buildingLimits == null || string . IsNullOrEmpty ( buildingName )) return - 1 ;
foreach ( var l in buildingLimits )
{
if ( l == null || l . buildingName != buildingName || l . maxByTownHall == null || l . maxByTownHall . Length == 0 ) continue ;
return Mathf . Max ( 0 , l . maxByTownHall [ Mathf . Clamp ( townHallLevel , 1 , l . maxByTownHall . Length ) - 1 ]);
}
return - 1 ;
}
// Время стройки (targetLevel = 1) или улучшения до targetLevel, мс. Ратуша -- с множителем
public long ConstructionMs ( bool townHall , int targetLevel )
{
var c = construction ;
double seconds ;
if ( targetLevel <= 1 ) seconds = c . buildSeconds ;
else
{
var t = c . upgradeSecondsToLevel ;
seconds = t == null || t . Length == 0 ? c . buildSeconds : t [ Mathf . Clamp ( targetLevel - 2 , 0 , t . Length - 1 )];
}
if ( townHall ) seconds *= Mathf . Max ( 0.01f , c . townHallTimeMultiplier );
return ( long )( Math . Max ( 0 , seconds ) * 1000.0 );
}
// Сколько ресурса не отнять при грабеже: часть запаса, которую прячут Склады и Амбары
public float GetProtected ( ResourceType type , int warehouseLevels , int granaryLevels )
{
var p = protection ;
switch ( type )
{
case ResourceType . Wood :
case ResourceType . Stone :
case ResourceType . Iron :
case ResourceType . Gold : return p . baseGoods + p . perWarehouseLevel * warehouseLevels ;
case ResourceType . Food : return p . baseFood + p . perGranaryLevel * granaryLevels ;
2026-11-04 23:36:14 +03:00
case ResourceType . Gems : return float . PositiveInfinity ; // без «Грабежа» кристаллы не грабятся (с ним -- ResourceProtection)
2026-11-02 00:01:51 +03:00
default : return 0f ;
}
}
2026-10-27 18:51:35 +03:00
// Цена улучшения здания с уровня level на level + 1
public ResourceAmount [] UpgradeCost ( int level )
{
float m = Mathf . Pow ( upgrade . multiplierPerLevel , Mathf . Max ( 0 , level - 1 ));
return new []
{
new ResourceAmount ( ResourceType . Wood , Mathf . Round ( upgrade . baseWood * m )),
new ResourceAmount ( ResourceType . Stone , Mathf . Round ( upgrade . baseStone * m )),
};
}
2026-10-27 17:59:16 +03:00
public float GetStartResource ( ResourceType type )
{
var s = startResources ;
switch ( type )
{
case ResourceType . Wood : return s . wood ;
case ResourceType . Food : return s . food ;
case ResourceType . Stone : return s . stone ;
case ResourceType . Iron : return s . iron ;
case ResourceType . Gold : return s . gold ;
case ResourceType . Gems : return s . gems ;
default : return 0f ;
}
}
2026-11-06 16:13:43 +01:00
2026-11-06 16:18:28 +01:00
// Цена ускорения в золоте: сколько осталось секунд -> золото (не меньше 1). Стройка, улучшение и обучение войск
public static int SpeedUpGold ( double secondsLeft )
{
int perGold = Mathf . Max ( 1 , Load (). construction . secondsPerGold );
return Mathf . Max ( 1 , Mathf . CeilToInt (( float )( secondsLeft / perGold )));
}
2026-11-06 16:13:43 +01:00
// ---------------- Хранилище синих кристаллов (регламент валют 05.11, разд. 8; правила -- CrystalStorage) ----------------
// Правила с купленной ступенью (её хранит Экономика: ResourceManager.CrystalStorageLevel, секция "economy")
public static CrystalStorage CrystalStorageNow ()
{
var c = Load ();
var tiers = new List < CrystalStorage . Tier >();
if ( c . storage . crystalExpansions != null )
foreach ( var e in c . storage . crystalExpansions )
{
if ( e == null ) continue ;
var cost = new List < ResourceAmount >();
if ( e . gold > 0 ) cost . Add ( new ResourceAmount ( ResourceType . Gold , e . gold ));
if ( e . wood > 0 ) cost . Add ( new ResourceAmount ( ResourceType . Wood , e . wood ));
if ( e . stone > 0 ) cost . Add ( new ResourceAmount ( ResourceType . Stone , e . stone ));
if ( e . iron > 0 ) cost . Add ( new ResourceAmount ( ResourceType . Iron , e . iron ));
tiers . Add ( new CrystalStorage . Tier { TownHallLevel = e . townHallLevel , Capacity = e . capacity , Cost = cost . ToArray () });
}
var storage = new CrystalStorage ( Mathf . RoundToInt ( c . storage . crystalCap ), tiers );
if ( ResourceManager . Instance != null ) storage . SetLevel ( ResourceManager . Instance . CrystalStorageLevel );
return storage ;
}
// Купить следующую ступень: цена списывается один раз (всё или ничего), ступень и ресурсы -- в одной секции
// сохранения "economy", сохраняется сразу. error -- почему нельзя
public static bool TryExpandCrystalStorage ( int townHallLevel , out string error )
{
var rm = ResourceManager . Instance ;
var storage = CrystalStorageNow ();
error = storage . WhyNot ( townHallLevel );
if ( error != null ) return false ;
if ( rm == null || ! GameServices . TryGet ( out IEconomy economy )) { error = "Склад не найден" ; return false ; }
if (! storage . TryExpand ( townHallLevel , cost => economy . TrySpend ( cost ))) { error = "Не хватает ресурсов" ; return false ; }
rm . CrystalStorageLevel = storage . Level ;
rm . SetCapacity ( ResourceType . Gems , storage . Capacity );
if ( SaveManager . Instance != null ) SaveManager . Instance . SaveGame ();
return true ;
}
2026-10-27 17:59:16 +03:00
}